Tabs

A row of tabs and the panels they switch between, with a swipeable pager.

Open Source

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 tabs

Usage

app/index.tsx
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.

Every variant
<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.

Every size
Every size
<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.

Swipeable — the default
<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.

Twelve tabs
<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.

Composed icons and a render prop
Composed icons and a render prop
<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.

Separators
Separators
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Separator betweenValues={["overview", "activity"]} />
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>

API Reference

Anatomy

PartWhat it is
TabsThe root. Owns the order walk, the value flow and the pan
Tabs.ListThe bar: the track, and the frame every trigger measures itself into
Tabs.ScrollViewA horizontal scroller for a bar with more tabs than room
Tabs.IndicatorThe one layer that slides. Write it out as the row's first child
Tabs.TriggerOne tab. A Pressable, so it inherits the whole vocabulary
Tabs.LabelA trigger's text. Bare string children become one automatically
Tabs.SeparatorA hairline between two tabs, which retreats as either is approached
Tabs.ContentOne 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

On this page