A tab group is a named table of tab keys, each with a display title and an optional icon, defined once for a whole documentation set. It buys you two things on the published page. Every tabbed example that uses the group looks identical — the same label, the same icon, in the same order, on every page. And the reader’s choice follows them: pick Mac once, and every operating-system example in your documentation is already on Mac, on every page they open afterwards, for as long as their browser remembers.
That second payoff is the whole design constraint of the feature, and most of this page is about living with it well.
The key is what @tab matches, not the title
Each entry in a group is stored under a key. When the build renders a tab, it lower-cases the text you typed after @tab and looks that up as the key. The configured key is not normalized on the way in.
Two consequences fall out of that one line:
- A key containing a capital letter can never match anything, because the left side of the comparison is always lower-cased.
- The lookup is exact, not fuzzy.
@tab C#will not find a key namedcsharp.
The failure mode matters more than the rule, because it is completely silent. An unknown key, or a group id that does not exist, produces no warning and no build failure:
| What you wrote | What renders | Does it still sync? | Is there a warning? |
|---|---|---|---|
| A key that is in the group | The entry’s title, with its icon | Yes | — |
| A key that is not in the group | The raw text you typed, case preserved, no icon element at all | Yes | No |
| A group id that does not exist | Raw text for every tab, no icons | Yes | No |
| No group id on the container | Raw text for every tab, no icons | No | No |
A typo does not break the page. It produces a tab that merely looks a bit unstyled next to its neighbors — which is why it survives review.
Membership is fixed, because syncing is by position
The rule that follows is unglamorous and non-negotiable: every container bound to a group lists every one of that group’s tabs, in the group’s key order. When a tab genuinely does not apply on a given page, write a panel that says so — “Windows is not supported for this command; use WSL and follow the Linux tab” — rather than dropping the tab. One extra panel is cheap. A reader silently redirected to the wrong instructions is not.
The exemption matters as much as the rule. A page-scoped ad-hoc id — a group id that no other container anywhere uses — syncs with nothing, because there is nothing else in its group. Such a container carries exactly the tabs its page needs, in whatever order suits it, and never pads its membership.
Bind every container to a group
Give every tab container a group id, including genuine one-offs. Not because a one-off will sync with anything, but because binding is what stops it inheriting.
A container with no group attribute that follows a grouped container on the same page picks up the previous container’s titles and icons while emitting no group of its own. It looks grouped, and it does not sync — the worst of both. An ad-hoc id costs one attribute and removes the problem entirely. The trade is that an ad-hoc id supplies no titles and no icons — only a configured group does that — so its @tab labels render exactly as typed.
The ===tabs-container syntax itself, the @tab lines, the blank-line rules and everything tabs do outside a documentation set are owned by Tabs. Only the attribute matters here:
===tabs-container {data-tabs-groupId="operating-system"}Where the configuration lives
There is no single copy that serves both consumers, so a working set has two.
| Copy | Edited at | Read by | If it is the only copy |
|---|---|---|---|
| Page type | Settings → Page Types → your set → Documentation | The site build | The group works on your published site and is missing from the editor’s Tab Group menu |
| Workspace | Settings → General → Documentation → Tab Groups | The editor’s Tab Group menu | The group appears in the toolbar and renders on the site with no titles and no icons |
Set both. The workspace copy propagates down to your documentation and API page types automatically, but fill-only: a new group is added, a changed title or icon never overwrites what the page type already has, and a removal never deletes anything. That is deliberate — it stops a workspace-level edit from silently rewriting a set someone has tuned — but it does mean the two copies drift once they both exist. Get a group right before it propagates, or edit the page-type copy directly afterwards.
One consequence for icons specifically: an icon class is only compiled into your site’s stylesheet if the class string appears somewhere in your site repository. A group that exists only in the CMS and has never been published can therefore render an icon element with no rule behind it. Publishing the page type fixes it, because that is what writes the group into the set’s data file. The full field-by-field reference for the object it lives in is at Documentation Configuration Reference.
The entry fields
Opening a group in the edit modal shows its derived id — generated from the group name and not editable — above the list of entries, each with its own title, icon and order.
| Field | Required? | What it controls | Read by | Example |
|---|---|---|---|---|
title | yes | The label printed on the tab, replacing whatever the author typed after @tab | Site build and CMS editor | Claude Code |
icon | no | The complete Font Awesome class pair rendered before the label — family and name, not a bare icon name | Site build | fa-brands fa-linux |
iconClass | no | Extra classes appended to icon | Site build | text-blue-600 |
iconStyle | no | Emitted as a raw inline style attribute on the icon. Keep it to custom properties | Site build | --fa-color: red |
codeBlockLanguage | no | Seeds each inserted tab with an empty code block in that language instead of an empty paragraph | CMS editor only | bash |
order | no | Sorts the tabs the toolbar inserts, and the rows in the edit modal | CMS editor only | 0 |
A group has no other fields. There is no per-group title, no per-group icon and no default tab — a group is nothing but a bag of tabs.
The reserved API Languages group
Every workspace has an api-languages group. It is created automatically from your API snippet languages — cURL, TypeScript, Python and Go by default — repaired automatically if entries are missing their icons, and deliberately filtered out of the editor’s Tab Group menu.
The editor’s Tab Group menu lists the groups you have defined. api-languages is deliberately absent from it: the group exists and renders, but it is maintained from your API snippet languages rather than inserted by hand.
It is product-generated rather than authored: the language tabs on a generated API reference page emit it directly. Do not rename it, trim its roster, restyle it (it uses a different icon weight from the others on purpose) or borrow it as a general code-language group for hand-written pages — the code regenerates and repairs it, so it will fight every edit. If you want language tabs on a hand-written page, give that page its own ad-hoc id. What the group actually drives is covered at API Reference Pages.
Designing a good set
The test for whether something deserves a predefined group is not “is this code” and not “would tabs look tidy here”. It is:
Is this a choice the reader makes once and keeps for the whole documentation set?
That is not a matter of taste. It falls straight out of the mechanism above: the choice is stored per group and replayed on every container in that group, forever. For a sticky identity — my operating system, my MCP client, my language — that is exactly right. For a per-task choice it is a bug generator: pick Video on the assets page and every later container in that group forces you to Video, including the ones you opened in order to read about images.
So keep groups small — two to four tabs, because fixed membership is the price of syncing — keep them closed, and put the recommended path first, since the first tab is the one that opens by default. Everything that fails the test gets a page-scoped ad-hoc id instead, or a table.
The groups these docs define
| Group id | Tabs, in key order | The sticky choice it represents | Where it is used |
|---|---|---|---|
mcp-clients | Claude · Claude Code · ChatGPT · Cursor | Which AI client the reader connects with | Every page that shows how to connect to Leed’s MCP servers |
operating-system | Linux · Mac · Windows | The reader’s operating system | Installing the CLI, and the keyboard-shortcut tables |
api-languages | cURL · TypeScript · Python · Go | The reader’s HTTP client language | Generated API reference pages — reserved and product-generated |
Two groups, plus the reserved one. Every other tabbed comparison in these docs — including the three documentation layouts, which readers compare rather than commit to — uses a page-scoped ad-hoc id and syncs with nothing.
Here is the operating-system group as it is stored, and then as it renders:
"operating-system": {
"linux": { "title": "Linux", "icon": "fa-brands fa-linux", "order": 0 },
"mac": { "title": "Mac", "icon": "fa-brands fa-apple", "order": 1 },
"windows": { "title": "Windows", "icon": "fa-brands fa-windows", "order": 2 }
}- Linux
- Mac
- Windows
You are reading the Linux panel, and this docs site has just remembered that. Open Installing the Leed CLI or Keyboard Shortcuts and each will already be showing you the Linux column.
You are reading the Mac panel, and this docs site has just remembered that. Every other operating-system container in these docs is now on Mac too — including the ones on pages you have not opened yet.
You are reading the Windows panel, and this docs site has just remembered that. Note that all three panels exist in every operating-system container in these docs, even where one of them only says “not supported here” — that is the fixed-membership rule in practice.
On the published page the two containers stay in step: selecting Python in one switches the other to Python at the same moment, because both read the same group.
flowchart LR
A["Reader clicks a tab<br/>in container 1"] --> B["Selection stored<br/>against the group"]
B --> C["Container 1 switches"]
B --> D["Container 2 switches<br/>to the same tab"]
Two things that look like bugs and are not
The editor’s Tab Group menu labels each group by title-casing its id, word by word. A group whose id is mcp-clients reads as Mcp Clients in that menu. It is cosmetic, it affects nothing that a reader ever sees, and there is no way to override it.
And an icon that looks correct in the CMS can render as a blank space on your site until the group has been published, for the compile reason described above. If an icon is missing on the published page but present in Settings, publish the page type before you go looking for a typo.
One note for anyone styling the strip
The selected-state rule has to key on the accessibility attribute — [aria-selected="true"] — and never on the active class the build stamps into the markup. That class lands on the first tab and never moves, because switching tabs updates ARIA state and panel visibility and leaves the class alone. A class-based rule underlines whichever tab the reader started on, permanently, no matter what they click. Everything else about theming the strip is at How Styling Works.
Icons come from the same bundled Font Awesome set your menus use, so the class pairs are interchangeable between the two — Building and Editing a Menu has the picker and the family names.