Tabs
A row of tabs and the panels they switch between, with a swipeable pager.
Installation
Tabs ships with the library — install it once, then import:
import { Tabs } from "@delacour/react-native-ui/tabs";Or copy the source into your project, to own and edit it:
bunx delacour@latest add tabsUsage
import { Tabs } from "@/components/ui/tabs";
import { Text } from "@/components/ui/text";
import { useState } from "react";
import { View } from "react-native";
export default function Example() {
const [tab, setTab] = useState<string | null>("all");
return (
<View className="flex-1 p-4">
<Tabs value={tab} onValueChange={setTab}>
<Tabs.List>
<Tabs.Indicator />
<Tabs.Trigger value="all">
<Tabs.Label>All</Tabs.Label>
</Tabs.Trigger>
<Tabs.Trigger value="unread">
<Tabs.Label>Unread</Tabs.Label>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="all">
<Text.Paragraph>Every message in the inbox.</Text.Paragraph>
</Tabs.Content>
<Tabs.Content value="unread">
<Text.Paragraph>Only the ones you have not opened yet.</Text.Paragraph>
</Tabs.Content>
</Tabs>
</View>
);
}Variants
primary is a fully rounded capsule inside a muted track; secondary is an underline rule.
<Tabs variant="primary">…</Tabs>
<Tabs variant="secondary">…</Tabs>Sizes
One axis drives the trigger's floor and padding, the gap ladder, the label's Text step and the
glyph step a composed Icon inherits.

<Tabs size="sm">…</Tabs>
<Tabs size="md">…</Tabs>
<Tabs size="lg">…</Tabs>Controlled
Pass value and onValueChange; omit both and the bar holds its own state from defaultValue.
<Tabs onValueChange={setTab} value={tab ?? null}>…</Tabs>value is string | null, never undefined
null means "controlled, nothing selected". Omitting the prop is what makes the bar uncontrolled,
so a useState<string>() seeded with undefined would silently hand the component its own state
and then switch it to controlled on the first press. Pass tab ?? null.
Swiping
isSwipeable is exactly one thing: whether the pan is enabled. Every panel mounts either way — use
Tabs.Content's function children for the per-panel mounting decision.
<Tabs isSwipeable={false}>…</Tabs>The pan claims sideways and gives up vertical, so a pager works inside
Screen.ScrollArea. A horizontal scrollable inside a panel is
yours to settle — the pager publishes its pan on useTabsMotion(), and you write
Gesture.Native().blocksExternalGesture(panGesture).
Scrolling
Wrap the row in a Tabs.ScrollView when the tabs genuinely do not fit. The bar tracks the finger
through a drag, and a hand-scroll always wins.
<Tabs.ScrollView>
<Tabs.List>…</Tabs.List>
</Tabs.ScrollView>Labels and icons
Selection changes the label's colour and nothing else — it crossfades off the same value the capsule
reads, while a composed Icon swaps at the midpoint.

<Tabs.Trigger value="inbox">
<Icon icon={IconInbox} />
<Tabs.Label>Inbox</Tabs.Label>
</Tabs.Trigger>A render prop must not change a trigger's size
A trigger's width is the indicator's geometry, so content that appears only on selection — a badge, a count — re-measures the row and shifts the whole bar on every tab change. Swap a treatment rather than adding or removing content.
Separators
A separator fades only while the pager is crossing it, and still takes its width while faded, so the row never reflows.

<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Separator betweenValues={["overview", "activity"]} />
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>API Reference
Anatomy
| Part | What it is |
|---|---|
Tabs | The root. Owns the order walk, the value flow and the pan |
Tabs.List | The bar: the track, and the frame every trigger measures itself into |
Tabs.ScrollView | A horizontal scroller for a bar with more tabs than room |
Tabs.Indicator | The one layer that slides. Write it out as the row's first child |
Tabs.Trigger | One tab. A Pressable, so it inherits the whole vocabulary |
Tabs.Label | A trigger's text. Bare string children become one automatically |
Tabs.Separator | A hairline between two tabs, which retreats as either is approached |
Tabs.Content | One panel. Must be a direct child of Tabs |
The panels' source order is the tab order. Triggers are not walked — each finds its place by looking
its value up in that list, so a trigger can sit behind a wrapper or come out of a .map().
Tabs.Content must be a direct child of Tabs. Triggers may be nested; panels may not. Writing the
triggers in a different order from the panels puts the indicator on the wrong tab, so the list checks
the measured positions ascend and warns by name in development.
Tabs
Prop
Type


