NavBar
Primary app navigation. The same component renders either horizontally
(top bar) or vertically (side rail), driven by one items tree. Items
can carry nested children to produce dropdowns on horizontal and
expand-collapse groups on vertical. Below the md breakpoint the bar
collapses to a hamburger that opens a Drawer containing the items —
opt out with responsive={false}.
The data shape#
items is a recursive tree of NavBarItem. Items without href
render as <button> (good for in-app navigation routed by state);
items with href render as <a> (good for SEO + native link
behavior); items with children produce a dropdown on horizontal or
an expand group on vertical.
const items: NavBarItem[] = [
{ id: 'home', label: 'Home', href: '/' },
{
id: 'products',
label: 'Products',
children: [
{ id: 'a', label: 'Product A', href: '/a' },
{ id: 'b', label: 'Product B', href: '/b' },
],
},
];Horizontal#
The default layout. Brand on the left, items in the middle, actions on
the right. Items without children render as plain links; items with
children open a dropdown via @radix-ui/react-navigation-menu.
Vertical#
Same data, side rail. Groups with children expand inline. The group
auto-opens when one of its descendants is the active item, so deep
links land on a discoverable item without a manual click.
With submenus (horizontal)#
Top-level items can declare children to open a dropdown. Disabled
children are still rendered but skip activation.
With expandable groups (vertical)#
Same children shape — the vertical layout shows them as collapsible
groups instead of dropdowns. Active state propagates to the parent
trigger so the visual hierarchy matches what the user is looking at.
Item (NavBarItem)#
A recursive shape — each item can carry its own children of the same
type.
id: string(required) — stable identifier. Whatvalue/onValueChangereference, and used to walk the tree for active-descendant detection.label: ReactNode(required) — visible label.icon?: ReactNode— optional left-of-label icon node.href?: string— when set, the item renders as<a>and fires native navigation alongsideonValueChange. Without it the item renders as<button>.badge?: ReactNode— trailing badge text (often a count, "new", or status indicator).disabled?: boolean— renders the item but skips activation. Keyboard navigation passes over it.children?: NavBarItem[]— nested items. Dropdowns on horizontal; expand-collapse groups on vertical.
Active state#
Active state is either controlled via value + onValueChange,
or uncontrolled via defaultValue. An item is "active" when its
id matches value; for groups, the trigger is treated as active
when any descendant is active.
const [active, setActive] = useState('overview');
<NavBar items={items} value={active} onValueChange={setActive} />;Items with hrefs#
When an item has an href, it renders as an <a> and onValueChange
fires alongside the native link navigation — convenient for keeping a
"current page" highlight in sync with router-driven navigation. Items
without href render as <button> elements, so keyboard activation
(Enter / Space) works without extra wiring.
Mobile / narrow viewports#
By default, the bar collapses to a top-row hamburger below md
(768px) and the items move into a left-side Drawer. Both
orientations share the same drawer body so users on small screens get
the vertical list regardless of which orientation the desktop layout
uses. Pass responsive={false} if your app handles its own responsive
behavior.
Top-level props#
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | enum | horizontal | Layout direction. Default `'horizontal'`. |
| items * | NavBarItem[] | — | Item tree driving the bar. |
| brand | ReactNode | — | Brand / logo slot rendered at the start. When `responsive` is `true`, `brand` also seeds the mobile Drawer's accessible name, so it should include text — e.g. `<><Logo /> ShipIt</>` rather than `<Logo />` alone. Falls back to `'Navigation'` when omitted. |
| actions | ReactNode | — | Trailing slot for secondary actions (avatar, settings, theme toggle, …). |
| value | string | undefined | — | Controlled active item id. |
| defaultValue | string | undefined | — | Uncontrolled initial active item id. |
| onValueChange | ((id: string) => void) | undefined | — | Fired when an item is activated. |
| width | number | undefined | 240 | Pixel width of the vertical rail. Default 240. |
| responsive | boolean | undefined | true | Collapse to a hamburger drawer below `md`. Default `true`. |