# Top tabs

> A swipeable pager under a tab strip whose indicator tracks the swipe continuously: the leading edge runs ahead and the trailing edge follows, so it stretches between tabs and gathers as the page lands. Labels recolor with the swipe and a scrolling strip keeps the indicator centred.

- Type: Component (navigation)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/top-tabs
- Markdown: https://goodui.dev/native/components/top-tabs.md
- Registry item: https://goodui.dev/native/r/top-tabs.json
- Dependencies: react-native-gesture-handler, react-native-reanimated
- Keywords: react native top tabs, material top tabs, swipeable tabs, view pager, tab view, scrollable tabs, tab indicator, expo pager

## When to use

- Sibling lists of one screen the person swipes between: upcoming, past and cancelled orders; all, unread and mentions.
- Many categories of a feed in a scrolling strip: for you, trending, food, travel.
- Profile or detail screens with sections that each scroll on their own.

## When not to use

- App-level destinations: use tab-bar at the bottom.
- Two to five filters of the same list without swiping between pages: use segmented-control.
- A row of cards or images to browse: use carousel.
- Many filters that combine: use chip.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/top-tabs.json
npx expo install --fix
```

The component lands in `components/goodui-native/top-tabs/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs badge, locale.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
import { View } from "react-native";
import { TopTabs } from "@/components/goodui-native/top-tabs/top-tabs";

export function OrdersScreen() {
  return (
    <View style={{ flex: 1 }}>
      <TopTabs
        accessibilityLabel="Orders"
        style={{ flex: 1 }}
        tabs={[
          { key: "upcoming", title: "Upcoming", badge: 2, content: <UpcomingOrders /> },
          { key: "past", title: "Past", content: <PastOrders /> },
          { key: "cancelled", title: "Cancelled", content: <CancelledOrders /> },
        ]}
      />
    </View>
  );
}
```

## Examples

### Scrolling strip with a pill

```tsx
<TopTabs
  variant="pill"
  size="sm"
  accessibilityLabel="Categories"
  style={{ flex: 1 }}
  tabs={categories.map((c) => ({ key: c.id, title: c.name, content: <Feed category={c.id} /> }))}
/>
```

### Controlled

```tsx
const [tab, setTab] = useState("all");

<TopTabs value={tab} onValueChange={setTab} style={{ flex: 1 }} tabs={tabs} />
<Button onPress={() => setTab("mentions")}>Show mentions</Button>
```

## API reference

### TopTabs

`import { TopTabs } from "@/components/goodui-native/top-tabs/top-tabs";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tabs` (required) | `TopTab[]` | – | Two or more tabs, in order. |
| `value` | `string` | – | Selected tab key (controlled). Pair it with `onValueChange`. |
| `defaultValue` | `string` | – | Selected tab key on first render (uncontrolled). Default the first tab. |
| `onValueChange` | `(key: string) => void` | – | Called when a tap or a swipe settles on a different tab. |
| `variant` | `"underline" \| "pill"` | `"underline"` | `underline` draws an accent line under the label; `pill` an accent capsule behind it. |
| `size` | `"sm" \| "md"` | `"md"` | `md` 48 pt tabs with subhead labels, `sm` 44 pt with footnote labels. |
| `scrollable` | `boolean \| "auto"` | `"auto"` | Scrolling strip of auto-width tabs instead of equal columns. `auto` (default) scrolls with more than four tabs. |
| `swipeEnabled` | `boolean` | `true` | Swipe between pages. Default `true`. |
| `lazy` | `boolean` | `true` | Mount a page only once it or a neighbour has been shown. Default `true`. |
| `haptic` | `HapticName \| false` | `"select"` | Haptic when the tab changes. `false` turns it off. |
| `accessibilityLabel` | `string` | – | Name of the tab list, for example "Orders". |
| `style` | `StyleProp<ViewStyle>` | – | Style of the whole component. Give it `flex: 1` (or a height) when pages scroll. |
| `tabBarStyle` | `StyleProp<ViewStyle>` | – | Style of the tab strip. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## Accessibility

- The strip is a tablist named by accessibilityLabel; each tab has the tab role with selected in accessibilityState and aria-selected on the web.
- Only the current page is exposed to screen readers. On the web the other pages are tabpanels made inert, so their controls also leave the tab order.
- Tab badges are read with the tab on the web and hidden as decoration on native (put the count in the title if it matters there).
- Tabs are at least 44 pt tall and as wide as their label plus padding.

## Motion

- Pages follow the finger 1:1 with resistance past the first and last page, and settle on the smooth spring carrying the release velocity, at most one page per swipe.
- The indicator is computed from the fractional page: its leading edge eases out and its trailing edge eases in between the two tabs' measured positions, so it stretches mid-swipe and lands exactly on the label (underline) or the tab (pill).
- Labels recolor by distance to their page, from secondary to the selected color.
- A scrolling strip scrolls each frame to keep the indicator centred.
- Tapping a tab glides the pager on the smooth spring, which carries the indicator with it.
- Reduced motion: tapping a tab swaps the page and fades it up instead of sliding; a swipe still follows the finger and settles with a short timing.

## Haptics

- select when a tap or a swipe settles on a different tab (configurable with haptic)

## Performance

- The pager, indicator, label colors and strip scrolling all run on the UI thread from one shared value; React state changes once per settled page.
- With lazy (default) a page mounts the first time it or a neighbour is current, and stays mounted afterwards to keep its scroll position.

## Notes for AI

- Give TopTabs style={{ flex: 1 }} (inside a bounded parent) when pages contain their own ScrollView or FlatList; otherwise pages size to the tallest content.
- content is any node; put the page's ScrollView inside it. Do not nest TopTabs pages in a horizontal ScrollView.
- Keep titles short; with more than four tabs (scrollable='auto') the strip scrolls and tabs size to their labels.
- Pressables inside a page take the pointer on the web, so a swipe that starts on a button does not move the pager there; swipes on the page background work.
- Use badge for counts; it hides at 0.

## Related

- [Tab bar](https://goodui.dev/native/components/tab-bar.md): A bottom tab bar whose pill stretches toward the next tab and can be dragged to scrub between tabs; the selected icon bounces, badges pop, and it can hide while scrolling.
- [Segmented control](https://goodui.dev/native/components/segmented-control.md): Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
- [Carousel](https://goodui.dev/native/components/carousel.md): A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.
- [Drawer](https://goodui.dev/native/components/drawer.md): A side drawer that follows the finger from the screen edge: the scrim darkens with its progress and, in the card variant, the screen behind shrinks, rounds and slides aside on the same progress. Release settles it with the finger's velocity; Escape, the back button or a tap on the scrim closes it.

## Guidance for AI tools

Top tabs: A swipeable pager under a tab strip whose indicator tracks the swipe continuously: the leading edge runs ahead and the trailing edge follows, so it stretches between tabs and gathers as the page lands. Labels recolor with the swipe and a scrolling strip keeps the indicator centred. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
