Top tabs
Free · MITReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/top-tabs.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/top-tabs.
Usage
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>
);
}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.
- 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.
Examples
Scrolling strip with a pill
<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
const [tab, setTab] = useState("all");
<TopTabs value={tab} onValueChange={setTab} style={{ flex: 1 }} tabs={tabs} />
<Button onPress={() => setTab("mentions")}>Show mentions</Button>API reference
| Prop | Type | Default |
|---|---|---|
| tabs* Two or more tabs, in order. | TopTab[] | — |
| value Selected tab key (controlled). Pair it with `onValueChange`. | string | — |
| defaultValue Selected tab key on first render (uncontrolled). Default the first tab. | string | — |
| onValueChange Called when a tap or a swipe settles on a different tab. | (key: string) => void | — |
| variant `underline` draws an accent line under the label; `pill` an accent capsule behind it. | "underline" | "pill" | "underline" |
| size `md` 48 pt tabs with subhead labels, `sm` 44 pt with footnote labels. | "sm" | "md" | "md" |
| scrollable Scrolling strip of auto-width tabs instead of equal columns. `auto` (default) scrolls with more than four tabs. | boolean | "auto" | "auto" |
| swipeEnabled Swipe between pages. Default `true`. | boolean | true |
| lazy Mount a page only once it or a neighbour has been shown. Default `true`. | boolean | true |
| haptic Haptic when the tab changes. `false` turns it off. | HapticName | false | "select" |
| accessibilityLabel Name of the tab list, for example "Orders". | string | — |
| style Style of the whole component. Give it `flex: 1` (or a height) when pages scroll. | StyleProp<ViewStyle> | — |
| tabBarStyle Style of the tab strip. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- 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.
- Haptic: select when a tap or a swipe settles on a different tab (configurable with haptic)
- 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.
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.
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
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.
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.
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.
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.