Skip to content

Tabs component

Tabs is a composable tabbed-navigation component with a roving-tabindex keyboard model built in — arrow keys move between tabs, only the active one sits in the tab order, and panels switch without a page reload. It can be driven declaratively with <Tab> children or dynamically from a data array via a render-function child.

Navigation27 propsAccessibility notes

Playground

Change a prop on the right and the preview updates. Switch to Code to copy what you see.

Recipes

Copy-paste patterns for the ways Tabs actually gets used — each one a real, self-contained example.

Basic tabs

Render <Tab> children with a title and panel content. Arrow-key navigation and the tablist/tabpanel ARIA pattern are built in — pass an aria-label describing the set.

<Tabs aria-label="Account settings">
  <Tab key="profile" title="Profile">Your public profile.</Tab>
  <Tab key="billing" title="Billing">Plans and invoices.</Tab>
  <Tab key="security" title="Security">Password and 2FA.</Tab>
</Tabs>

Variants

Four visual styles — solid, bordered, light, and underlined — so tabs fit anything from a card header to a full-page section switcher.

<Tabs aria-label="Style: solid" variant="solid">…</Tabs>
<Tabs aria-label="Style: bordered" variant="bordered">…</Tabs>
<Tabs aria-label="Style: light" variant="light">…</Tabs>
<Tabs aria-label="Style: underlined" variant="underlined">…</Tabs>

Accent color

color tints the active tab with a semantic tone — pair it with the underlined variant for a clean, branded section switcher.

<Tabs aria-label="Sections" variant="underlined" color="primary">
  <Tab key="overview" title="Overview">…</Tab>
  <Tab key="activity" title="Activity">…</Tab>
  <Tab key="settings" title="Settings">…</Tab>
</Tabs>

Vertical tabs

isVertical stacks the tab list; pair with placement="start" for a settings-style sidebar layout.

<Tabs aria-label="Settings" isVertical placement="start">
  <Tab key="general" title="General">…</Tab>
  <Tab key="notifications" title="Notifications">…</Tab>
  <Tab key="advanced" title="Advanced">…</Tab>
</Tabs>

Tabs with icons

A title can be any node — an icon beside a label. When the title isn't plain text, pass titleValue so assistive tech still gets a real name.

<Tabs aria-label="Views">
  <Tab
    key="grid"
    title={<span className="flex items-center gap-2"><GridIcon /> Grid</span>}
    titleValue="Grid"
  >
    …
  </Tab>
  <Tab
    key="home"
    title={<span className="flex items-center gap-2"><HomeIcon /> Home</span>}
    titleValue="Home"
  >
    …
  </Tab>
</Tabs>

Declarative tabs

For a fixed, known set of tabs, render <Tab> children directly — each one's title is the label, and its children are the panel content for that tab.

<Tabs aria-label="Account settings">
  <Tab title="Profile">
    <ProfileForm />
  </Tab>
  <Tab title="Billing">
    <BillingForm />
  </Tab>
  <Tab title="Security">
    <SecurityForm />
  </Tab>
</Tabs>

Dynamic tabs from data

When tabs come from an array (a list of projects, a set of API-driven categories), pass items and a render-function child instead of hand-writing a <Tab> per entry — the same underlying rendering, just data-driven.

<Tabs aria-label="Projects" items={projects}>
  {(project) => (
    <Tab key={project.id} title={project.name}>
      <ProjectSummary project={project} />
    </Tab>
  )}
</Tabs>

Controlled selection

Tabs manages its own selected tab by default (optionally seeded with defaultSelectedKey). Switch to selectedKey + onSelectionChange when something outside Tabs needs to react to or drive the active tab — syncing it to a URL query param, for instance.

const [tab, setTab] = useState("profile");

<Tabs selectedKey={tab} onSelectionChange={(key) => setTab(String(key))}>
  <Tab key="profile" title="Profile">...</Tab>
  <Tab key="billing" title="Billing">...</Tab>
</Tabs>

Placement and orientation

placement moves the tab list relative to its panels (top, bottom, start, end); isVertical switches the tab list itself to a vertical stack, useful paired with placement="start"/"end" for a sidebar-style tab layout.

<Tabs placement="start" isVertical aria-label="Settings">
  <Tab title="General">...</Tab>
  <Tab title="Notifications">...</Tab>
</Tabs>

Keyboard activation mode

keyboardActivation="automatic" (the default) selects a tab as soon as arrow-key focus reaches it. Switch to "manual" when moving focus shouldn't immediately trigger a (potentially expensive) panel change — the user arrows through tabs freely and only Enter/Space actually selects one.

Props

The full public API for Tabs. Every prop is stable and intentionally typed.

Content

children

ReactNode | ((item: T) => ReactElement)

default: —

Declarative <Tab> children, or a render function with items.

items

Iterable<T>

default: —

Dynamic items, used with a render-function children.

title

ReactNode

default: —

Tab label (required).

titleValue

string

default: —

Plain-text override of title, used for accessible naming when title is a node.

children

ReactNode

default: —

Panel content for this tab.

Appearance

variant

"solid" | "bordered" | "light" | "underlined"

default: solid

Visual style variant.

color

"default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger"

default: default

Semantic color.

size

"xs" | "xs-sm" | "sm" | "md" | "lg" | "xl"

default: md

Tab list size.

radius

"none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full"

default: full

Border radius.

Layout & positioning

fullWidth

boolean

default: false

Stretches tabs to fill the available width.

placement

"top" | "bottom" | "start" | "end"

default: top

Tab list position relative to panels.

isVertical

boolean

default: false

Renders a vertical tab list.

Behavior & states

disabledKeys

Key[]

default: —

Keys of tabs to disable.

selectedKey

Key

default: —

Controlled selected tab key.

defaultSelectedKey

Key

default: —

Uncontrolled initial selected tab key.

shouldSelectOnPressUp

boolean

default: true

Selects on pointer-up rather than pointer-down.

keyboardActivation

"automatic" | "manual"

default: automatic

Whether arrow-key focus also selects, or only Enter/Space does.

isDisabled

boolean

default: false

Disables the whole tab list.

destroyInactiveTabPanel

boolean

default: false

Unmounts inactive panels instead of hiding them.

href / target / rel / download / ping / referrerPolicy

string

default: —

Renders the tab as a link when href is set.

isDisabled

boolean

default: false

Disables this specific tab.

shouldSelectOnPressUp

boolean

default: —

Per-tab override of the Tabs-level setting.

Motion

disableCursorAnimation

boolean

default: false

Disables the sliding active-tab cursor animation.

disableAnimation

boolean

default: false

Disables all animation, including the cursor.

Events

onSelectionChange

(key: Key) => void

default: —

Called when the selected tab changes.

Styling

classNames

TabsClassNames

default: —

Per-slot class overrides (base/tabList/tab/tabContent/cursor/panel/tabWrapper).

className

string

default: —

Class applied to the root element.

Accessibility

  • Implements the standard tablist/tab/tabpanel ARIA pattern with roving tabindex — only the active tab is in the natural tab order; arrow keys move focus between the rest.
  • Disabled tabs (isDisabled, either on the whole Tabs or a single Tab) are skipped by arrow-key navigation, not just visually dimmed.
  • A Tab's accessible name comes from title; if title is a non-text ReactNode (an icon plus a label, say), pass titleValue with the plain-text equivalent so assistive tech still gets a real name.
  • Always pass Tabs its own aria-label (or aria-labelledby) describing what the tab set is for — "Account settings", "Projects" — since the tabs' own titles alone don't convey the group's purpose.

Best practices

  • Use Tabs for switching between views of related content within the same context — not as a substitute for real page-level navigation between unrelated sections.
  • Set destroyInactiveTabPanel when panels are expensive to keep mounted (a heavy chart, a form with its own side effects); leave it off (the default) when switching back to a tab should preserve its scroll position and in-progress state.
  • Prefer the declarative <Tab> children form for a small, fixed set of tabs, and the items + render-function form once the set is genuinely data-driven — trying to force a small static set through the dynamic form just adds indirection.
  • Keep tab titles short — a tab list isn't the place for a full sentence, and long titles fight the fixed-width layout most tab bars use.