Skip to content

Code tabs

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

.github/workflows/ci.yml
name: CI
on: push
jobs:
greet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: python greet.py
- run: node greet.js

You write

:::code-tabs
```yaml title=".github/workflows/ci.yml"
name: CI
on: push
jobs:
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 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.

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
npm install starlight-codeblocks

To show an icon on a tab with no title, give the block an icon, such as icon="pnpm".

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:

Python
import json
with open("config.json") as fh:
config = json.load(fh)
Python
with open("config.json", "w") as fh:
json.dump(config, fh, indent=2)

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:

config.yml
server:
port: 8080
  • 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 with icon.
    • 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 title of 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 for py. 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 sync key switch together, matched by label. The browser keeps the choice for each key in localStorage, 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 sync key 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 select element 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.

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

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:

astro.config.mjs
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.

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 .md file, use code tabs, or change the file to .mdx for <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:

Terminal window
brew install jq

You write

<Tabs syncKey="os">
<TabItem label="macOS">
Install the tool with Homebrew:
```sh
brew install jq
```
</TabItem>
<TabItem label="Windows">
Install the tool with winget:
```sh
winget 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.

  • 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.
  • Fill-in placeholders: one block with fields for the values that differ, instead of one variant for each value.