# Tab bar

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

- Type: Component (navigation)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/tab-bar
- Markdown: https://goodui.dev/native/components/tab-bar.md
- Registry item: https://goodui.dev/native/r/tab-bar.json
- Dependencies: react-native-gesture-handler, react-native-reanimated
- Keywords: react native bottom tab bar, expo router custom tab bar, animated tab bar, tab bar with badges, hide tab bar on scroll, floating tab bar, pill tab indicator

## When to use

- Top-level navigation between three to five sections of an app: home, search, inbox, profile.
- As the custom tabBar of expo-router Tabs through NavigationTabBar.
- Long feeds where the bar should step aside while reading (pass scrollY).

## When not to use

- Switching views of the same content inside a screen: use segmented-control.
- More than five destinations: move the rest into a profile or more screen.
- Actions such as compose or add: use a button or a trailing action in large-title-header.

## 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/tab-bar.json
npx expo install --fix
```

The component lands in `components/goodui-native/tab-bar/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs 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 { Tabs } from "expo-router";
import { NavigationTabBar } from "@/components/goodui-native/tab-bar/tab-bar";

export default function TabsLayout() {
  return (
    <Tabs tabBar={(props) => <NavigationTabBar {...props} variant="floating" />} screenOptions={{ headerShown: false }}>
      <Tabs.Screen name="index" options={{ title: "Home", tabBarIcon: ({ color, size }) => <HomeIcon color={color} size={size} /> }} />
      <Tabs.Screen name="inbox" options={{ title: "Inbox", tabBarBadge: 3, tabBarIcon: ({ color, size }) => <InboxIcon color={color} size={size} /> }} />
      <Tabs.Screen name="profile" options={{ title: "Profile", tabBarIcon: ({ color, size }) => <UserIcon color={color} size={size} /> }} />
    </Tabs>
  );
}
```

## Examples

### Standalone, controlled, with badges

```tsx
import { TabBar } from "@/components/goodui-native/tab-bar/tab-bar";

const [tab, setTab] = useState("chats");

<TabBar
  accessibilityLabel="Main"
  value={tab}
  onValueChange={setTab}
  onReselect={() => listRef.current?.scrollToOffset({ offset: 0 })}
  items={[
    { value: "chats", label: "Chats", icon: (p) => <ChatIcon {...p} />, badge: unread },
    { value: "calls", label: "Calls", icon: (p) => <PhoneIcon {...p} />, badge: true },
    { value: "profile", label: "Profile", icon: (p) => <UserIcon {...p} /> },
  ]}
/>
```

### Hide while scrolling

```tsx
const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler((e) => {
  scrollY.value = e.contentOffset.y;
});

<View style={{ flex: 1 }}>
  <Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: 120 }}>
    {content}
  </Animated.ScrollView>
  <TabBar variant="floating" absolute scrollY={scrollY} items={items} defaultValue="today" />
</View>
```

## API reference

### TabBar

`import { TabBar } from "@/components/goodui-native/tab-bar/tab-bar";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` (required) | `TabBarItem<T>[]` | – | Two to five tabs. |
| `value` | `T` | – | The selected tab (controlled). |
| `defaultValue` | `T` | – | The tab selected first when uncontrolled. Defaults to the first item. |
| `onValueChange` | `(value: T) => void` | – | Called with the new tab on tap, or on release after dragging the pill. |
| `onReselect` | `(value: T) => void` | – | Called when the selected tab is tapped again, for example to scroll to the top. |
| `variant` | `"docked" \| "floating"` | `"docked"` | `docked` spans the bottom edge with a hairline; `floating` is an inset capsule above the home indicator. |
| `showLabels` | `boolean` | `true` | Show labels under the icons. With `false` the bar is 52 pt tall and icon only. |
| `haptic` | `HapticName \| false` | `"select"` | Haptic when the tab changes. `false` turns it off. |
| `hidden` | `boolean` | `false` | Hide the bar (slides below the screen edge). Overrides the scroll behavior while true. |
| `scrollY` | `SharedValue<number>` | – | Vertical scroll offset of the active screen. With `hideOnScroll` the bar hides while scrolling down. |
| `hideOnScroll` | `boolean` | `true` | Hide while scrolling down and show again on scrolling up or near the top. Default `true` when `scrollY` is passed. |
| `absolute` | `boolean` | `false` | Pin the bar to the bottom of its parent so content scrolls under it. Use it with `hideOnScroll`. |
| `draggable` | `boolean` | `true` | Allow dragging the pill along the bar to scrub between tabs. Default `true`. |
| `style` | `StyleProp<ViewStyle>` | – | Style for the outer container. |
| `accessibilityLabel` | `string` | – | Overrides the label read by screen readers. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

### NavigationTabBar

`import { NavigationTabBar } from "@/components/goodui-native/tab-bar/tab-bar";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `state` (required) | `{ index: number; routes: { key: string; name: string; params?: object }[] }` | – | – |
| `descriptors` (required) | `Record<string, { options: NavTabOptions }>` | – | – |
| `emitter` | `{ emit(event: TabPressEvent): { defaultPrevented?: boolean } }` | – | expo-router: emits tabPress so listeners and scroll-to-top keep working. |
| `navigateToTab` | `(routeKey: string) => void` | – | expo-router: switches to the tab with this route key. |
| `navigation` | `{ emit(event: TabPressEvent): { defaultPrevented?: boolean }; navigate(name: string, params?: object): void; }` | – | React Navigation 7. |
| `style` | `StyleProp<ViewStyle>` | – | Style for the outer container. |
| `variant` | `"docked" \| "floating"` | – | `docked` spans the bottom edge with a hairline; `floating` is an inset capsule above the home indicator. |
| `scrollY` | `SharedValue<number>` | – | Vertical scroll offset of the active screen. With `hideOnScroll` the bar hides while scrolling down. |
| `showLabels` | `boolean` | – | Show labels under the icons. With `false` the bar is 52 pt tall and icon only. |
| `haptic` | `HapticName \| false` | – | Haptic when the tab changes. `false` turns it off. |
| `hidden` | `boolean` | – | Hide the bar (slides below the screen edge). Overrides the scroll behavior while true. |
| `hideOnScroll` | `boolean` | – | Hide while scrolling down and show again on scrolling up or near the top. Default `true` when `scrollY` is passed. |
| `absolute` | `boolean` | – | Pin the bar to the bottom of its parent so content scrolls under it. Use it with `hideOnScroll`. |
| `draggable` | `boolean` | – | Allow dragging the pill along the bar to scrub between tabs. Default `true`. |

## Accessibility

- The row has accessibilityRole tablist; each tab is a tab with accessibilityState selected (aria-selected on the web).
- Tab labels include the badge: 'Chats, 3 new', or 'Calls, new' for a dot. Override with accessibilityLabel per item.
- Every tab cell is at least 44 pt tall and fills an equal share of the width.
- On the web tabs are focusable; Enter and Space select.
- On the web a tab is named by its visible label plus visually hidden badge text, and the drawn badge is aria-hidden.

## Motion

- Selection: the pill's leading edge travels on the snappy spring and the trailing edge on the smooth spring, so it stretches toward the new tab and settles.
- Drag: the pill follows the finger 1:1 along the bar, clamped to the ends; release projects the velocity and snaps to the nearest tab.
- Icon and label colors cross-fade in proportion to how much of the pill covers each tab, so they follow a dragged pill.
- The newly selected icon dips to 0.8 and springs back on the bouncy spring; a badge count that goes up pops to 1.35 and settles.
- Hide on scroll: after 24 pt of downward scrolling the bar slides below the edge on the smooth spring; 24 pt upward or reaching the top brings it back.
- Reduced motion: the pill jumps instead of stretching, no icon bounce or badge pop, and hiding becomes a fade.

## Haptics

- select when the tab changes on tap
- select on every tab boundary while dragging the pill (the release itself is quiet)

## Performance

- Pill, colors, bounce and hiding run on the UI thread; React renders only when the value or the items change.
- Each icon renders twice (on and off colors) for the cross-fade, so keep icon components light.

## Notes for AI

- With expo-router use NavigationTabBar in Tabs' tabBar prop; it reads title or tabBarLabel, tabBarIcon and tabBarBadge from the screen options and emits tabPress so scroll-to-top listeners still work.
- icon is a render function receiving { color, size, focused }; color the icon with the color it receives.
- badge: a number shows a count (99+ above 99), true shows a dot, 0 or false hides it.
- For hideOnScroll pass the screen's scroll offset as a shared value and set absolute so content scrolls under the bar; add bottom padding to the content.
- Do not wrap it in SafeAreaView: the docked bar pads itself with the bottom inset and the floating bar sits above it.

## Related

- [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.
- [Large title header](https://goodui.dev/native/components/large-title-header.md): An iOS large title that collapses into a compact bar as the content scrolls: the titles cross-fade, the bar fades in, the title stretches when pulled past the top and a half-finished collapse settles on either state.
- [Bottom sheet](https://goodui.dev/native/components/bottom-sheet.md): A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.

## Guidance for AI tools

Tab bar: 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. 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
