# Bottom Tab Bar

> A React bottom tab bar for phones: 3 to 5 tabs at the thumb with an animated pill, badges, a raised center action and hide on scroll.

- Type: React component (Mobile)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add bottom-tab-bar`
- Docs: https://wingo-ui.com/components/bottom-tab-bar

## Interaction

Tap a tab and the pill glides to it on a quick spring while the icon pops and swaps to its filled version; the icon dips while your finger is down. Tap the tab you are already on to scroll its page back to the top, or, on a nested page, to go back to the tab's first screen. The raised center button presses like a button and runs its action. Counts and dots sit on the icons and bounce in when they change. With hideOnScroll the bar follows your finger down out of view and comes back as soon as you scroll up or reach the end. On a keyboard Tab reaches every tab, ArrowLeft / ArrowRight move between them, Home / End jump to the first and last, Enter opens; the bar never hides while keyboard focus is inside it. On a phone it clears the home indicator (safe area) and every tab is far bigger than 44px. Under reduced motion the pill jumps and nothing pops.

## Installation

```bash
npx wingo-ui@latest add bottom-tab-bar
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/bottom-tab-bar` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/bottom-tab-bar.tsx`

npm dependencies: `npm i motion`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useScrollState](https://wingo-ui.com/components/use-scroll-state) ([Markdown](https://wingo-ui.com/components/use-scroll-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
"use client"

import { House, Inbox, Plus, UserRound, Users } from "lucide-react"
import { BottomTabBar, type BottomTabBarItem } from "@/components/ui/bottom-tab-bar"

const tabs: BottomTabBarItem[] = [
  { label: "Home", href: "/", icon: <House />, activeIcon: <House className="fill-current" /> },
  { label: "Clients", href: "/clients", icon: <Users />, activeIcon: <Users className="fill-current" /> },
  { label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
  { label: "Profile", href: "/profile", icon: <UserRound />, activeIcon: <UserRound className="fill-current" /> },
]

export function MobileNav({ onCreate }: { onCreate: () => void }) {
  return (
    <BottomTabBar
      className="md:hidden"
      items={tabs}
      action={{ label: "Add", icon: <Plus />, onSelect: onCreate }}
      hideOnScroll
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `BottomTabBarItem[]` |  | 3 to 5 tabs (a dev warning above 5). The shared NavItem shape: id, label, href, icon, activeIcon (shown while active, e.g. a filled glyph), badge (a number is a count, a node renders as is), disabled, match, onSelect; plus dot: true for an 8px dot instead of a count. Items without href are buttons. |
| `value?` | `string` |  | The active tab's key (navItemKey: id, else href, else the label). Controls the bar. |
| `defaultValue?` | `string` |  | The first active tab while uncontrolled and no current path is known. Default: the first enabled tab. |
| `onValueChange?` | `(key: string, item: BottomTabBarItem) => void` |  | Fires whenever another tab is activated, in every mode (controlled, router, uncontrolled). |
| `currentPath?` | `string` | `""` | The current pathname. Empty: the LinkProvider's. With a path the router decides the active tab (the longest matching href) and clicks store nothing. Try /inbox or /clients/northwind-tech. |
| `action?` | `{ label: string; icon: ReactNode; onSelect?: () => void; href?: string }` |  | The raised center button (Add, Compose, Scan), placed in the middle of the tabs (with 4 tabs: 2, action, 2). label becomes its aria-label; filled with the tone. |
| `variant?` | `"bar" \| "floating"` | `"bar"` | bar: full width on the bottom edge with a glass surface and a hairline (iOS). floating: an inset glass pill above the safe area (iOS 26), the pill indicator behind the whole tab. |
| `indicator?` | `"pill" \| "line" \| "dot" \| "none"` | `"pill"` | pill: a 56x32 tinted pill behind the icon (Material 3). line: a 2px bar on the top edge of the tab. dot: a 4px dot under the label. none: color only. Every one moves between tabs on a snappy spring. |
| `showLabels?` | `"always" \| "active" \| "never"` | `"always"` | active: only the active tab shows its label and the others center their icon. never: icons only; the labels stay for screen readers and as tooltips. |
| `size?` | `"sm" \| "md"` | `"md"` | Bar height 56 / 64px, icons 22 / 24px, labels 10 / 11px. Both stay far above 44px targets. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The active color (neutral: the foreground, with a gray pill) and the fill of the center action. Other tones tint the pill and the active label. |
| `color?` | `string` | `""` | Any CSS color. Replaces the tone for the active tab and the action. Leave empty to use the tone. |
| `badgeTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"danger"` | The fill of count badges and dots. Red by default: on every platform people read a red count as unread. |
| `badgeLabel?` | `string` | `"unread"` | Read by screen readers after a count: "Inbox, 4 unread". The visible badge is hidden from them. |
| `dotLabel?` | `string` | `"new"` | Read by screen readers after a dot: "Profile, new". |
| `aria-label?` | `string` | `"Primary"` | The name of the <nav> landmark. |
| `hideOnScroll?` | `boolean` | `false` | Slides down while you scroll down, following the finger, and comes back on scroll up, at the end of the content and whenever focus is inside. Transform only, no re-render per frame. |
| `position?` | `"auto" \| "fixed" \| "sticky" \| "static"` | `"auto"` | auto: fixed to the viewport, or static inside an app shell whose content column scrolls. sticky: sticky bottom-0 as the last child of a scroll container (phone frames, demos). static: in the flow. Writes --bottom-bar-offset (on <html> when fixed, else on its parent) so floating buttons clear it. |
| `safeArea?` | `boolean` | `true` | Pads env(safe-area-inset-bottom) so the tabs clear the iPhone home indicator (the floating pill keeps at least 12px). It is 0 in desktop browsers. |
| `haptics?` | `boolean` | `true` | A 10ms vibration on a tab change where the browser supports it (Android; iOS ignores it). |
| `scrollToTopOnReselect?` | `boolean` | `true` | Tapping the active tab scrolls its page to the top (iOS). On a nested page the tab first goes back to its root. |
| `onReselect?` | `(item: BottomTabBarItem) => void` |  | Runs when the active tab is tapped again (refresh a feed, focus a search). |
| `scrollTarget?` | `HTMLElement \| Window \| RefObject<HTMLElement \| null> \| null` |  | What scrolls, for hide on scroll and reselect. Default: the nearest ScrollContainerProvider, else the window. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for this bar only (a component, or "a"). Usually set once for the app with LinkProvider. |
| `classNames?` | `Partial<Record<"inner" \| "list" \| "link" \| "icon" \| "label" \| "indicator" \| "action", string>>` |  | Extra classes for the inner parts: the bar surface, the list, each tab link, icon box, label, the indicator and the action. |
| `className?` | `string` |  | Extra classes for the <nav>, merged last. Set [--bar-bg:var(--surface)] when the bar sits on a surface other than the page. |

## Parts

### BottomTabBarItem (type)

One tab: the shared navigation item (label, href, icon, activeIcon, badge, disabled and the rest of NavItem) plus dot, an 8px dot in place of a count.

### BottomTabBarAction (type)

The raised center button (compose, add, scan): { label, icon, onSelect?, href? }.

## Examples

- **Floating**: variant="floating": an inset glass pill over a photo feed.
- **Indicators**: pill, line, dot or color only; each moves between tabs on a snappy spring.
- **Labels**: Always, only on the active tab, or never (kept for screen readers).
- **Hide on scroll**: The bar follows your finger out of view and returns on scroll up.
- **Three to five tabs**: Three tabs without an action; four and five tabs around a raised center action.
- **Live badges**: Counts bounce when they change and read as "Inbox, 5 unread".
- **Brand marketplace**: tone="brand" with the line indicator and a camera action.

## See also

- [Topbar](https://wingo-ui.com/components/topbar) ([Markdown](https://wingo-ui.com/components/topbar.md))
- [Large Title Header](https://wingo-ui.com/components/large-title-header) ([Markdown](https://wingo-ui.com/components/large-title-header.md))
- [Gooey Nav](https://wingo-ui.com/components/gooey-nav) ([Markdown](https://wingo-ui.com/components/gooey-nav.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [useScrollState](https://wingo-ui.com/components/use-scroll-state) ([Markdown](https://wingo-ui.com/components/use-scroll-state.md))

---

Canonical: https://wingo-ui.com/components/bottom-tab-bar
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
