Tabs
In-page tabs that switch panels without a navigation. For sections that are their own URL, use LinkTabs.
Examples
Panels
Arrow keys move between tabs. Panels are plain markup: no islands inside.
Run the command below, then restart Claude Code.
Add the URL under Settings, then MCP, then Add server.
Add a [mcp_servers.switchboard] entry to ~/.codex/config.toml.
<Tabs
label="Setup instructions"
items={[
{ name: 'claude-code', label: 'Claude Code', panel: <Text size="sm" tone="ink-2">Run the command below, then restart Claude Code.</Text> },
{ name: 'cursor', label: 'Cursor', panel: <Text size="sm" tone="ink-2">Add the URL under Settings, then MCP, then Add server.</Text> },
{ name: 'codex', label: 'Codex', panel: <Text size="sm" tone="ink-2">Add a [mcp_servers.switchboard] entry to ~/.codex/config.toml.</Text> },
]}
/>Counts, default and disabled
defaultTab picks the open tab; otherwise the first enabled one opens.
26 tools on GitHub.
8 tools are visible to agents.
No failing tools.
<Tabs
label="Server tools"
defaultTab="enabled"
items={[
{ name: 'all', label: 'All', count: 26, panel: <Text size="sm">26 tools on GitHub.</Text> },
{ name: 'enabled', label: 'Enabled', count: 8, panel: <Text size="sm">8 tools are visible to agents.</Text> },
{ name: 'failing', label: 'Failing', count: 0, disabled: true, panel: <Text size="sm">No failing tools.</Text> },
]}
/>Usage
import { Tabs } from '@switchboard/ui'
<Tabs
label="Setup instructions"
items={harnesses.map((h) => ({ name: h.id, label: h.name, panel: <SetupSteps harness={h} /> }))}
/>Props
Every component also takes mix, for css() and mixins applied after its own style.
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | – | Accessible name of the tab list. |
| items* | TabItem[] | – | { name, label, count?, disabled?, panel } per tab. |
| defaultTab | string | – | Name of the tab open at first. Defaults to the first enabled tab. |
Island: props cross to the browser, so panels must be static markup and css() only (no nested islands, on() or ref()).
TabItem
| Prop | Type | Default | Description |
|---|---|---|---|
| name* | string | – | Stable id for the tab. |
| label* | string | – | Tab text. |
| count | number | string | – | Trailing count. |
| disabled | boolean | – | Shown but not selectable. |
| panel* | RemixNode | – | Panel content, serialized as markup. |
Accessibility
- Built on @remix-run/ui/tabs: tablist, tab and tabpanel roles, Left and Right arrows, Home and End, roving focus.
- The ARIA attributes are in the server HTML, so the markup is correct before hydration.
- Each panel has tabindex="0" so its content can be reached with Tab even when it has no links.
Guidance
Do
- Use Tabs for alternatives of the same content, like setup steps per harness.
- Keep tab labels to one or two words.
Don’t
- Use Tabs for page sections with their own URL; use LinkTabs.
- Put islands (copy buttons, menus, switches) inside a panel; they will not hydrate.