Examples often come in several files, or in versions for each package manager or language. Stacked one after the other, they fill the page with code that most readers skip. Code tabs show them as one block, with a tab for each block in the title bar, as in a code editor.
Readers see
name: CIon: pushjobs: greet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: python greet.py - run: node greet.jsimport sys
name = sys.argv[1] if len(sys.argv) > 1 else "world"print(f"Hello, {name}!")const name = process.argv[2] ?? 'world';console.log(`Hello, ${name}!`);You write
:::code-tabs```yaml title=".github/workflows/ci.yml"name: CIon: pushjobs: greet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: python greet.py - run: node greet.js``````py title="greet.py"import sys
name = sys.argv[1] if len(sys.argv) > 1 else "world"print(f"Hello, {name}!")``````js title="greet.js"const name = process.argv[2] ?? 'world';console.log(`Hello, ${name}!`);```:::Select greet.py, and the block shows the Python file. Each tab shows the title of its block, with its file icon.
Syntax
Section titled “Syntax”| Syntax | Where |
|---|---|
:::code-tabs … ::: |
Around the code blocks |
sync="<key>" |
After :::code-tabs, in braces |
control="tabs" or control="menu" |
After :::code-tabs, in braces |
label="<text>" |
Code block fence line of each variant |
Put one or more fenced code blocks between the :::code-tabs line and the ::: line. The directive can contain only code blocks. Anything else, such as a paragraph, fails the build with a message that names the file.
Each code block is a variant. A tab shows the title of its variant. A variant with no title shows its label, and a variant with no label shows the name of its language, such as “Python” for py. The label also matches variants across blocks with the same sync key. Two variants with the same label fail the build. For example, sh and bash both show “Shell”, so give each one a label.
Examples
Section titled “Examples”Package managers
Section titled “Package managers”A tab with no title shows the label, with no file icon. Blocks with the same sync key switch together, on this page and on the other pages of the site. Select pnpm, and the install command on the home page changes too:
npm install starlight-codeblockspnpm add starlight-codeblocksyarn add starlight-codeblocks:::code-tabs{sync="pm"}```sh label="npm"npm install starlight-codeblocks``````sh label="pnpm"pnpm add starlight-codeblocks``````sh label="Yarn"yarn add starlight-codeblocks```:::To show an icon on a tab with no title, give the block an icon, such as icon="pnpm".
Switch by language
Section titled “Switch by language”Two blocks with the same sync key and no labels or titles switch together by language. Select JavaScript in one block, and the other block changes too:
import json
with open("config.json") as fh: config = json.load(fh)import { readFile } from 'node:fs/promises';
const config = JSON.parse(await readFile('config.json', 'utf8'));let text = std::fs::read_to_string("config.json")?;let config: serde_json::Value = serde_json::from_str(&text)?;with open("config.json", "w") as fh: json.dump(config, fh, indent=2)await writeFile('config.json', JSON.stringify(config, null, 2));std::fs::write("config.json", serde_json::to_string_pretty(&config)?)?;:::code-tabs{sync="lang"}```pyimport json
with open("config.json") as fh: config = json.load(fh)``````jsimport { readFile } from 'node:fs/promises';
const config = JSON.parse(await readFile('config.json', 'utf8'));``````rustlet text = std::fs::read_to_string("config.json")?;let config: serde_json::Value = serde_json::from_str(&text)?;```:::
:::code-tabs{sync="lang"}```pywith open("config.json", "w") as fh: json.dump(config, fh, indent=2)``````jsawait writeFile('config.json', JSON.stringify(config, null, 2));``````ruststd::fs::write("config.json", serde_json::to_string_pretty(&config)?)?;```:::A menu in place of tabs
Section titled “A menu in place of tabs”With control="menu", the title bar shows the title of the variant that shows, and a menu on the right that picks the variant. The menu shows the icon of the language, and names each variant by its label. Here, a tool reads its settings from YAML or TOML:
server: port: 8080[server]port = 8080:::code-tabs{control="menu"}```yaml title="config.yml"server: port: 8080``````toml title="config.toml"[server]port = 8080```:::Behaviour
Section titled “Behaviour”- The title bar:
- Only one variant shows at a time.
- Tabs: the title bar has a tab for each variant, in the order of the blocks. The tab of the variant that shows is the editor tab of Expressive Code. Each tab has the file icon of its
title, or the icon that the block names withicon. - Tabs use the editor frame, also for shell commands, which otherwise get a terminal frame.
- If the tabs do not fit, the row of tabs scrolls sideways.
- Menu: the title bar shows the
titleof the variant, if it has one, and the menu on the right. The menu starts with the icon of the language of the variant, such as the Python logo forpy. A language with no icon gets a generic code icon.
- Switching variants:
- Selecting a tab, or an entry in the menu, shows that variant. Keyboard focus moves to the same tab or the menu in the new variant.
- Blocks with the same
synckey switch together, matched by label. The browser keeps the choice for each key inlocalStorage, so it applies across the site and on the next visit. - If a block does not have the saved label, it shows its first variant.
- A block with no
synckey switches on its own, and the browser does not keep the choice.
- Keyboard and screen readers:
- The tabs are a tab list with the accessible name “Variant”. Tab moves focus to the selected tab. Left Arrow and Right Arrow select the previous and next tab, and Home and End select the first and last tab.
- The menu is a native
selectelement with the accessible name “Variant”.
- Copy and print:
- The copy button copies the variant that shows.
- The variant that shows prints, with its own tab. The other tabs and the menu do not print.
- Without JavaScript:
- The first variant shows, with its own tab. The other tabs and the menu do not show.
Options
Section titled “Options”codeTabs
Section titled “codeTabs”Combines several variants of a block, with tabs or a menu in the title bar. Set it to false to turn the feature off.
- Type
false | object- Default
- On
codeTabs.control
Section titled “codeTabs.control”Editor tabs in the title bar, or a menu on its right. A :::code-tabs directive can set its own with control="…".
- Type
'tabs' | 'menu'- Default
'tabs'
To use the menu on every block of the site, set control to 'menu'. A directive with control="tabs" still shows tabs:
codeblocks({ codeTabs: { control: 'menu' },});With codeTabs: false, each code block in the directive renders on its own, one after the other. The directive lines do not show.
Code tabs or Starlight tabs
Section titled “Code tabs or Starlight tabs”Starlight has a <Tabs> component that also shows one version at a time and remembers the choice across pages. The two differ in what they can contain and in how they look.
| Property | Code tabs | <Tabs> |
|---|---|---|
| Contains | Code blocks only | Any content: prose, lists, code blocks, components |
| Control | Editor tabs or a menu in the title bar of the block | A row of tabs above the content |
| Works in | Markdown and MDX | MDX only |
| Keeps the choice | With sync="<key>" |
With syncKey="<key>" |
| Space on the page | One code block | One code block, plus the row of tabs |
- If every version is one code block, use code tabs.
- If a version needs prose, a list or a second code block, use
<Tabs>. - If the page is a
.mdfile, use code tabs, or change the file to.mdxfor<Tabs>.
For example, the steps to install a tool can differ for each operating system, with a sentence of explanation before each command:
Readers see
Install the tool with Homebrew:
brew install jqInstall the tool with winget:
winget install jqlang.jqYou write
<Tabs syncKey="os"><TabItem label="macOS">
Install the tool with Homebrew:
```shbrew install jq```
</TabItem><TabItem label="Windows">
Install the tool with winget:
```shwinget install jqlang.jq```
</TabItem></Tabs>The two keys are separate. Code tabs with sync="pm" and a <Tabs> group with syncKey="pm" do not switch together. Use one of the two for each kind of choice on a site.
Limitations
Section titled “Limitations”- The directive works in Markdown and MDX files. It does not work around a
<Code>component. - Labels match by their exact text. “npm” and “NPM” are two different labels.
- A saved choice applies after the page loads, so a block can show its first variant for a moment.
Related
Section titled “Related”- Fill-in placeholders: one block with fields for the values that differ, instead of one variant for each value.