The vertical bar of the app's top-level pages on the start edge of a wide screen, as in a chat or mail app or an editor's activity bar: each item an icon over a short label, the current one with its currentIcon, a bar on the rail's edge and aria-current="page". An item can carry a count, capped at "99+" on the badge and spoken in full, be a button for an app that switches views without its router (onClick without href), and sit after a { type: "separator" }. A header (your app's mark) sits above the items and a footer (settings, the account) below them.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/navigation-rail#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/navigation-railIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/navigation-rail#v1.3.10 ourostack/ouro-ui/navigation-rail-tests#v1.3.10import { NavigationRail } from "@/components/ouro/navigation-rail/navigation-rail";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<NavigationRail
label="Main"
currentPath={pathname}
header={<AppMark />}
items={[
{ label: "Activity", href: "/activity", icon: <BellIcon />, currentIcon: <BellFilledIcon />, count: 3 },
{ label: "Chat", href: "/chat", icon: <ChatIcon />, currentIcon: <ChatFilledIcon /> },
{ label: "Calendar", href: "/calendar", icon: <CalendarIcon /> },
{ type: "separator" },
{ label: "Files", href: "/files", icon: <FolderIcon /> },
]}
footer={<SettingsButton />}
/>Usage
The rail is one tab stop: Up and Down move between its items, wrapping, and Home and End go to the ends, while Tab moves on to the footer and the page. Items that do not fit the rail's height move, from the last, into a More menu at its end; the current item stays on the rail. Labels wrap to two lines between words; one still too long ends in an ellipsis and shows whole in a Tooltip, as every label does with hideLabels. The current item's bar grows in from its middle; with reduced motion it only fades.
Links render through OuroProvider's linkComponent, so your router navigates. The rail fills its container's height and draws no surface of its own, so it sits on your app's frame.
Use instead when
- Use
TabBarinstead for the bar at the bottom of a phone screen (hide the rail there withclassName="max-sm:hidden"),Sidebarfor a column of labelled links in sections, andNavbarfor a few links across the top.
Props
Type: NavigationRailProps.
className- Type:
string - Layout only: placement and width, or
max-sm:hiddenwhere aTabBartakes over. Appearance classes warn in development. countLabel- Type:
string - Default:
"{count} new" - What screen readers hear after an item's label when it has a
count, with{count}for the number: "Activity, 5 new". currentPath- Type:
string - The path of the page being shown, from your router (
useLocation().pathname, orusePathname()in Next.js). The item whosehreforpathsmatch it, or else the longest one above it (/chatfor/chat/12), getsaria-current="page"and the current look. footer- Type:
ReactNode - Below the items, out of their way: a settings
Buttonor an accountMenu. header- Type:
ReactNode - Above the items: your app's mark, or a workspace
Menu. hideLabels- Type:
boolean - Default:
false - Shows the icons alone, in a 56px rail; each item's label names it for screen readers and shows in a Tooltip on hover and keyboard focus.
items(required)- Type:
readonly NavigationRailEntry[] - The items, in order, with
{ type: "separator" }between groups. Those that do not fit the rail's height move, from the last, into a More menu at its end; the current item stays on the rail. label(required)- Type:
string - Names the navigation for screen readers ("Main"). It is not shown.
moreLabel- Type:
string - Default:
"More" - The label of the button that holds the items that do not fit.
ref- Type:
Ref<HTMLElement> - Reaches the
<nav>.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
children: Not rendered. The rail isitems,headerandfooter.collapsed: Not a prop.hideLabelsshows the icons alone.links: Not a prop. Ouro UI usesitems:<NavigationRail label="Main" items={[{ label, href, icon }]} />.onSelect: Not a prop. Give the items their ownonClick.pathname: Not a prop. Ouro UI usescurrentPath:<NavigationRail currentPath={pathname}>.selectedId: Not a prop. Mark the shown view's itemcurrent: true, and switch views in each item'sonClick.value: Not a prop. The current item followscurrentPath, or an item'scurrent.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
NavigationRailButtonItem
A button in a NavigationRail, for an app that switches views without its router: give the shown view's item current.
interface NavigationRailButtonItem extends NavigationRailItemBase {
/** What the item does: show its view. */
onClick: (event: MouseEvent<HTMLElement>) => void;
/** Not with `onClick` alone. Give a link `href`; a button has none. */
href?: never;
/** Not on a button. A button is current by its `current` flag. */
paths?: never;
}NavigationRailEntry
One entry in a NavigationRail's items: an item, or a separator between groups.
type NavigationRailEntry = NavigationRailItem | NavigationRailSeparator;NavigationRailItem
One item of a NavigationRail: a link (href) or a button (onClick).
type NavigationRailItem = NavigationRailLinkItem | NavigationRailButtonItem;NavigationRailLinkItem
A link in a NavigationRail: one of the app's top-level pages.
interface NavigationRailLinkItem extends NavigationRailItemBase {
/** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
href: string;
/**
* Other paths whose pages belong to this item, so it stays current there: a Chat item that
* also owns `/people`. Each matches its own path and the pages under it.
*/
paths?: readonly string[] | undefined;
/** Called when the link is followed, before your router navigates. */
onClick?: ((event: MouseEvent<HTMLElement>) => void) | undefined;
}NavigationRailSeparator
A short rule between groups of items, as Menu's { type: "separator" } is.
interface NavigationRailSeparator {
type: "separator";
}