---
title: "Code tabs"
description: "Show several code blocks as one, with editor tabs in the title bar, such as the files of a project or the commands for each package manager."
url: "https://ewels.github.io/starlight-codeblocks/features/code-tabs/"
markdown: "https://ewels.github.io/starlight-codeblocks/features/code-tabs.md"
section: "Adapt to the reader"
site: "starlight-codeblocks documentation"
context: "This page is from the documentation of starlight-codeblocks. A Starlight plugin that adds focus, line states, annotations, API auto-linking and 22 more features to the code blocks of a site."
index: "https://ewels.github.io/starlight-codeblocks/llms.txt"
---

# Code tabs

> Show several code blocks as one, with editor tabs in the title bar, such as the files of a project or the commands for each package manager.

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.

````md
:::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

| 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

### 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](https://ewels.github.io/starlight-codeblocks/) changes too:

````md
:::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`](https://ewels.github.io/starlight-codeblocks/features/file-icons/), such as `icon="pnpm"`.

### 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:

````md
:::code-tabs{sync="lang"}
```py
import json

with open("config.json") as fh:
    config = json.load(fh)
```
```js
import { readFile } from 'node:fs/promises';

const config = JSON.parse(await readFile('config.json', 'utf8'));
```
```rust
let text = std::fs::read_to_string("config.json")?;
let config: serde_json::Value = serde_json::from_str(&text)?;
```
:::

:::code-tabs{sync="lang"}
```py
with open("config.json", "w") as fh:
    json.dump(config, fh, indent=2)
```
```js
await writeFile('config.json', JSON.stringify(config, null, 2));
```
```rust
std::fs::write("config.json", serde_json::to_string_pretty(&config)?)?;
```
:::
````

### 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:

````md
:::code-tabs{control="menu"}
```yaml title="config.yml"
server:
  port: 8080
```
```toml title="config.toml"
[server]
port = 8080
```
:::
````

## 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 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". <kbd>Tab</kbd> moves focus to the selected tab. <kbd>Left Arrow</kbd> and <kbd>Right Arrow</kbd> select the previous and next tab, and <kbd>Home</kbd> and <kbd>End</kbd> 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.

## Options

### `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`

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:

```js title="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.

- [Configuration](https://ewels.github.io/starlight-codeblocks/configuration/): Learn how to set options for the whole site, or for a single code block.

## 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 `.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:

````md
<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.

## 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

- [Fill-in placeholders](https://ewels.github.io/starlight-codeblocks/features/fill-in-placeholders/): one block with fields for the values that differ, instead of one variant for each value.
