BetaNavigation

Tabs

A row of tabs, and what each one shows.

Overview

A row of tabs, and what each one shows.

First example

<Tabs defaultValue="all">
<Tabs.List>
<Tabs.Indicator />
<Tabs.Trigger value="all">Tout</Tabs.Trigger>
<Tabs.Trigger value="unread">Non lus</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="all"></Tabs.Content>
</Tabs>

Anatomy

<Tabs>
  <Tabs.List>
    <Tabs.Indicator />
    <Tabs.Trigger value="…">
      <Tabs.Label>…</Tabs.Label>
    </Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="…">…</Tabs.Content>
</Tabs>
  • Tabs — holds what is chosen, and every trigger's measured rectangle.
  • Tabs.List — the row the triggers sit in, and the box the indicator slides inside.
  • Tabs.Trigger — one tab. It measures itself.
  • Tabs.Label — its text.
  • Tabs.Indicator — the one node that says which tab is chosen.
  • Tabs.Content — what a tab shows.

Usage

Basic

<Tabs defaultValue="all">
  <Tabs.List>
    <Tabs.Indicator />
    <Tabs.Trigger value="all">Tout</Tabs.Trigger>
    <Tabs.Trigger value="unread">Non lus</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="all">…</Tabs.Content>
</Tabs>

A stringifiable trigger becomes its label (R3), which is what makes the common case one line per tab.

Three shapes

<Tabs variant="light">…</Tabs>
varianttrackindicatorchosen label
primaryfilled, roundeda pill behind itsegmentForeground
secondarya hairline undera two-point ruleforeground
lightnonenoneaccent

Three affordances rather than the same one louder, which is why the union is three rather than the usual four.

light is the quietest a tab bar can be and still be one: no track, no rule, nothing but the chosen tab's label going to the accent. The colour is the whole signal, which is why it goes to the accent rather than to plain ink — a tab merely darker than its neighbours is not chosen, it is just darker. It belongs over content that is already busy, and not where a bar has to be found before it can be read.

Tabs.Indicator draws nothing under light, whether you leave it in or take it out.

Widening the bar

<Tabs.List style={{ alignSelf: 'stretch' }}>
  <Tabs.Indicator />
  <Tabs.Trigger value="all" style={{ flex: 1 }}>
    Tout
  </Tabs.Trigger>
</Tabs.List>

The list hugs its tabs by default. A tab bar as wide as the screen with three tabs in it is a segmented control pretending to be a navigation bar, so widening it is a decision you make rather than one you undo.

No indicator

Leave Tabs.Indicator out and the label's colour is the only thing saying which tab is chosen. That is a legitimate bar, and it is why the indicator is a slot rather than something the list conjures.

Style as props

<Tabs.List padding={6} />
<Tabs.Trigger paddingHorizontal={20} />
<Tabs.Label fontSize={18} />

Full RN names, full RN values (R14). Every node takes them.

Root props

TabsProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
variantTabsVariant | undefinedNo
sizeTabsSize | undefinedNo
radiusRadiusKey | undefinedNo
colorstring | undefinedNoThe tint (R7) — a raw value, never a token.
valuestring | undefinedNo
defaultValuestring | undefinedNo
onValueChange((value: string) => void) | undefinedNo
isDisabledboolean | undefinedNo

Slots

TabsContentProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
valuestringYes
childrenReactNodeNo

TabsIndicatorProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

TabsLabelProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

TabsListProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

TabsTriggerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
valuestringYes
isDisabledboolean | undefinedNo
childrenReactNode | ((state: TabsTriggerRenderState) => ReactNode)No
asChildboolean | undefinedNo

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

The list is a tablist, each trigger a tab carrying selected, each panel a tab.

Migration from legacy

Legacyv1
<Tabs tabs={[…]} />one <Tabs.Trigger> per tab
activeIndexvalue — a string, not a position
onIndexChangeonValueChange
variant="segmented"variant="primary"
variant="underline"variant="secondary"
customAppearance={{ … }}style on the slot that key named

A tab is named, not numbered. An index breaks the moment a tab is inserted, and it is what made the legacy component's activeIndex a prop nobody could hold correctly.

Implementation notes

Motion

The indicator is one node sliding, not a border on each tab appearing and disappearing. The triggers publish their rectangles on layout, the root keeps them, and the indicator springs between them on the UI thread — so it keeps travelling while whatever the new tab shows is mounting.

Softer than the chevron's spring: damping 20 against stiffness 220 at mass 0.6. That one turns 180 degrees and must not overshoot; this one slides a few dozen points, and a touch of overshoot is what makes it feel attached to the press.

The first placement jumps rather than springing. Animating it would slide the pill in from the start of the row on mount, which reads as the tab bar arranging itself rather than as a control at rest. Until a rectangle exists the indicator renders at zero opacity, so there is no flash at the start of the row either.

Not here yet

A scrollable list. The reference implementation's Tabs.ScrollView centres the chosen tab when the bar overflows, and that means the indicator has to account for a scroll offset the triggers' own layout does not report. Worth its own change.

Tabs.Separator. A hairline between tabs in the secondary shape.