A bar of top-level destinations fixed to one edge of the viewport, translucent or solid, attached or floating, whose labels can show always, only for the active item, or never.
1
2
3'use client';
import { Compass, House, Inbox, Search, User } from 'lucide-react';Installation#
pnpm dlx shadcn@latest add @zui/tab-barUsage#
import {
TabBar,
TabBarBadge,
TabBarIcon,
TabBarIndicator,
TabBarItem,
TabBarLabel,
} from '@/components/navigation/tab-bar';<TabBar aria-label="Main" labels="selected">
<TabBarItem render={<Link href="/" />} active={pathname === '/'}>
<TabBarIndicator>
<TabBarIcon>
<House />
</TabBarIcon>
</TabBarIndicator>
<TabBarLabel>Home</TabBarLabel>
</TabBarItem>
<TabBarItem render={<Link href="/inbox" />} active={pathname === '/inbox'}>
<TabBarIndicator>
<TabBarIcon>
<Inbox />
</TabBarIcon>
<TabBarBadge>3</TabBarBadge>
</TabBarIndicator>
<TabBarLabel>Inbox</TabBarLabel>
</TabBarItem>
</TabBar>Each item is a link to a route, and the route the app is on sets active. Pass the router's link through
render so the item navigates on the client; without it the item is a plain <a>. Give the bar an
aria-label naming what its destinations are, since a page can hold more than one nav.
side takes the edges shadcn's Sheet names: bottom and top lay the items in a row, left and right
in a column. labels="selected" shows only the active label and labels="none" shows none; a hidden label
is visually hidden, so a screen reader still reads every item's name. Inside an element with contain: layout it is fixed to
that element instead, as in the preview.
1
2
3import { House, Search, User } from 'lucide-react';
import type { ReactNode } from 'react';
Clearing the bar and the device#
A fixed bar sits over the page, so it can cover the last content and a focused control that scrolls under
it. Pad the page by the bar's height on its side, for example pb-24 for a bottom bar, and give anything
that takes focus a scroll-margin of the same amount so the browser scrolls it clear of the bar.
On a phone with a home indicator or a notch, the top and bottom bars add env(safe-area-inset-top) or
env(safe-area-inset-bottom) to their padding, so the indicator does not cover the items. The browser
reports those insets as zero unless the page's viewport meta tag includes viewport-fit=cover:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />In Next.js, set viewportFit: 'cover' in the viewport export.
Composition#
TabBar
`-- TabBarItem
|-- TabBarIndicator
| |-- TabBarIcon
| `-- TabBarBadge
`-- TabBarLabelAPI reference#
TabBar#
A nav that takes every prop a nav takes, and requires aria-label. It has no border: the surface
separates by blur and shadow.
| Prop | Type | Default |
|---|---|---|
aria-label | string | |
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' |
labels | 'always' | 'selected' | 'none' | 'always' |
surface | 'translucent' | 'solid' | 'translucent' |
shape | 'attached' | 'floating' | 'attached' |
translucent is bg-background/70 backdrop-blur, and solid is bg-background. attached runs edge to
edge and casts its shadow away from that edge, and floating is a pill 16px in from the edges. It carries data-side, data-orientation
(horizontal or vertical), data-labels, data-surface and data-shape, and sets
data-slot="tab-bar".
TabBarItem#
An a that takes every prop an a takes, and render to render another element, such as the router's
link. Its name is its TabBarLabel.
| Prop | Type | Default |
|---|---|---|
active | boolean | false |
active sets aria-current="page", lights the indicator and keeps the label under labels="selected". In
a row the items share the width; in a column each is as wide as the bar. Each is at least 48px high,
with its label hidden or not. An aria-describedby the caller passes is kept, and the badge's id is added
to it. While it has keyboard focus its TabBarIndicator is ringed, or the item itself when it holds no
indicator. It carries data-active while active and sets data-slot="tab-bar-item".
TabBarIndicator#
A span that takes every prop a span takes: the 56 by 32 pill behind the icon, tinted while its item is
active, muted while pressed and ringed while the item has keyboard focus. It carries data-active and sets
data-slot="tab-bar-indicator".
TabBarIcon#
A span that takes every prop a span takes, sizing the glyph in its children to 20px. It is hidden from a
screen reader, which reads the label. It sets data-slot="tab-bar-icon".
TabBarBadge#
Renders shadcn's Badge, destructive, at the indicator's
top right, and takes every prop it takes except variant. Its children are the count.
| Prop | Type | Default |
|---|---|---|
countLabel | string | 'new' |
The badge is kept out of the item's name and describes the item instead, with countLabel read after the
count, so a screen reader hears "Inbox, link, 3 new". Pass countLabel in the app's language. It sets
data-slot="tab-bar-badge".
TabBarLabel#
A span that takes every prop a span takes, text-xs font-medium. Its children are the destination's
name. Where the bar's labels hides it, it is visually hidden and carries data-hidden. It sets
data-slot="tab-bar-label".