Skip to main content

Tabs

3 min readStableBeginner

Tabs renders a row of tab buttons over a single visible panel, and each TabItem supplies one tab's label and its content. The tab strip is a real ARIA tablist: arrow keys move between tabs and switch the panel, wrapping from the last tab back to the first, Home jumps to the first tab, End to the last, and focus follows the selection. Every example below shows the live component first, then the exact MDX that produced it.

Quick Start

Two tabs and nothing else — the first TabItem is active on load.

React uses a virtual DOM and JSX syntax for building user interfaces.

app/features/components/tabs.mdx
MDX
<Tabs>
  <TabItem value="react" label="React">
    React uses a virtual DOM and JSX syntax for building user interfaces.
  </TabItem>
  <TabItem value="vue" label="Vue">
    Vue provides a progressive framework with an approachable template syntax.
  </TabItem>
</Tabs>

Adding Options

Add a third tab and set defaultValue to the value of whichever tab should open first.

Svelte shifts work to compile time, producing vanilla JavaScript.

app/features/components/tabs.mdx
MDX
<Tabs defaultValue="svelte">
  <TabItem value="react" label="React">
    React uses a virtual DOM and JSX syntax.
  </TabItem>
  <TabItem value="vue" label="Vue">
    Vue provides a progressive framework with reactive data binding.
  </TabItem>
  <TabItem value="svelte" label="Svelte">
    Svelte shifts work to compile time, producing vanilla JavaScript.
  </TabItem>
</Tabs>

Advanced

syncKey writes the selected value to localStorage under tabs-<syncKey>, so a reader who picks pnpm here sees pnpm on the next page that uses the same key. Five tabs also show the other half of the strip's behavior: it scrolls sideways instead of wrapping onto a second row, so a narrow screen keeps one tidy line.

npm install next

app/features/components/tabs.mdx
MDX
<Tabs defaultValue="npm" syncKey="package-manager">
  <TabItem value="npm" label="npm">
    `npm install next`
  </TabItem>
  <TabItem value="yarn" label="yarn">
    `yarn add next`
  </TabItem>
  <TabItem value="pnpm" label="pnpm">
    `pnpm add next`
  </TabItem>
  <TabItem value="bun" label="bun">
    `bun add next`
  </TabItem>
  <TabItem value="deno" label="deno">
    `deno add npm:next`
  </TabItem>
</Tabs>

Reuse package-manager on another page and both groups need the same value set. A stored value that no tab on the page matches leaves the group with nothing selected and no panel showing.

Options

Tabs

defaultValuestringDefault: first tab's value

The value of the tab that opens first. Match it to a real TabItem: a value no tab carries selects nothing and shows no panel, instead of falling back to the first tab. A stored syncKey selection overrides it on load.

syncKeystring

Key used to save and restore the selection in localStorage as tabs-<syncKey>. Reading and writing are wrapped in try/catch, so blocked storage leaves the tabs working. Omit it and the choice resets on every visit.

childrenReactNoderequired

One or more TabItem elements. A child without both value and label is skipped, so it renders no tab button and no panel. Tabs keep their source order, and the strip scrolls horizontally once the labels outgrow the content width.

TabItem

valuestringrequired

Identifier for the tab. Keep it unique within one Tabs group — it pairs the tab button with its panel and is the value defaultValue and syncKey match against.

labelstringrequired

Text shown on the tab button.

childrenReactNoderequired

Panel content. It is rendered only while this tab is the active one, so a hidden panel's markup never reaches the page.

Two Tabs groups on the same page can share the same value strings without colliding. Each group generates its own React id prefix, and the id, aria-controls, and aria-labelledby attributes are built from it.

Was this page helpful?