Skip to content

Navigation

Tabs

Switch between panels of related content, with the full tab keyboard contract.

Examples

Line tabs

The default for page and panel-level navigation in dense UI: one row of triggers, a hairline underneath, and a single indicator that slides between them.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

Surface tabs

Inside a panel, where the strip is a view switcher rather than navigation: the indicator becomes a raised plate and the container carries the well.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

Three sizes

`sm` lives in a card header, `md` is the default, and `lg` carries a page-level view switch. The height is the only thing that changes.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

A strip that fills its region

`fill` gives every trigger an equal share of the width, which is what a full-width card header or a two-option switch wants.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

Selection the caller owns

Pass `value` and `onValueChange` and the strip reports the switch — which is how a tab becomes part of the URL, of a saved view, or of a query.

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

Reported value

metrics

In a chart panel header

The shape a product actually uses: a title, a badge and a small surface strip sharing one line, with the panel's own body underneath.

BTC/USD · 1d

+2.1%

The panel swaps instantly. The indicator carries the state so the label hierarchy can stay quiet.

Usage

import { Tabs } from "@luc/ui";
<Tabs defaultValue="overview">
  <Tabs.List>
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="metrics">Metrics</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="overview">…</Tabs.Content>
  <Tabs.Content value="metrics">…</Tabs.Content>
</Tabs>

`Tabs.List`, `Tabs.Trigger` and `Tabs.Content` are the same import as `Tabs`; a trigger and its panel are paired by `value`.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

`Tabs` is one symbol carrying its own parts: `.List` (adds `fill`), `.Trigger` (adds `leadingIcon`) and `.Content`. The table below is the root's props, which is where `variant`, `size` and the controlled pair live; the parts take the Radix props their names imply.

Exported symbols
ExportKindSignature
Tabs9 documented props(({ className, variant, size, value, defaultValue, onValueChange, children, ...props }: TabsProps) => import("@types/react").JSX.Element) & { List: ({ className, fill, children, ref, ...props }: Tabs…

Tabs props

Type: TabsProps

Values the compiler reads from the declaration.

Tabs props
PropTypeDefaultNotes
activationMode?inherited"manual" | "automatic" | undefined—Whether a tab is activated automatically or manually.
asChild?inheritedboolean | undefined—
defaultValue?inheritedstring | undefined—The value of the tab to select by default, if uncontrolled
dir?inheritedDirection | undefined—The direction of navigation between toolbar items.
onValueChange?inherited((value: string) => void) | undefined—A function called when a new tab is selected
orientation?inherited"horizontal" | "vertical" | undefined—The orientation the tabs are layed out. Mainly so arrow navigation is done accordingly (left & right vs. up & down)
size?TabsSize | undefined—
value?inheritedstring | undefined—The value for the selected tab, if controlled
variant?TabsVariant | undefined—

Guidelines

Do

  • Use `Tabs` for alternative views of one subject, where the label is the navigation and the reader can leave and come back.
  • Give every enabled trigger a `Tabs.Content`, even an empty one, so the `aria-controls` relationship always resolves to a panel.
  • Reach for `variant="surface"` when the strip sits inside a panel and `fill` when the triggers should divide the available width.
  • Let the shared indicator carry the selection: it measures the two adjacent triggers and slides on the emphasized curve, which is why the labels can stay quiet.

Don't

  • Do not use tabs for a value that is submitted with a form: that is a `SegmentedControl`, which is a toggle group and reports its value, while a tab reports which panel is showing.
  • Do not grow the active label's weight — the row reflows and the shared indicator's own geometry moves with it.
  • Do not put two tab strips in one region with different meanings; the reader has no way to tell which one the arrows belong to.

Accessibility

Radix owns the tab contract: one Tab stop for the strip, arrow keys move between triggers, Home and End jump to the ends, and each trigger carries `aria-selected` and `aria-controls`. The shared indicator is `aria-hidden` decoration, so the active state never depends on the animation — and under `prefers-reduced-motion` the indicator does not animate at all, it arrives.

Luc UI v1.0Tabsdark theme