Navigation
Sidebar
The column of links down the side of an app, in sections, with the current page marked aria-current="page" and a bar at the start edge, distinct from hover and press highlights. An item can carry a badge ("Inbox 12"), be a button (onClick without href), or hold one level of nested links (items). From 640px up it fills its container's height and collapses to a rail of icons with its own button, each named by a Tooltip, where an item with nested links opens them in a Menu; below 640px it is a menu button that opens the same links as a Sheet drawer. Links render through OuroProvider's linkComponent, so your router navigates.
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.2.13Then 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/sidebar#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/sidebarIts 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/sidebar#v1.2.13 ourostack/ouro-ui/sidebar-tests#v1.2.13import { Sidebar } from "@/components/ouro/sidebar/sidebar";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Sidebar
label="Main"
header={<Link href="/">Spoonjoy</Link>}
sections={[
{ items: [{ label: "Recipes", href: "/recipes", icon: <BookIcon /> }] },
{ label: "Library", items: [{ label: "Cookbooks", href: "/cookbooks", icon: <StackIcon /> }] },
]}
currentPath={pathname}
/>Usage
The phone menu opens once JavaScript has loaded (after hydration); the links are in the server HTML from 640px up.
Put it in a SidebarLayout, which places it beside the page and in the phone's top bar.
Use instead when
- Use
Navbarinstead for a handful of links across the top, andTabsfor views of one page.
Props
Type: SidebarProps.
className- Type:
string - Layout only: grid placement and margins (
row-span-2). Appearance classes warn in development. closeLabel- Type:
string - Default:
"Close" - The label of the drawer's close button.
collapsed- Type:
boolean - Whether the sidebar is collapsed, when you control it (to remember it). Pair it with
onCollapsedChange. collapseLabel- Type:
string - Default:
"Collapse sidebar" - The collapse button's name and text;
aria-pressedsays whether it is on. collapsible- Type:
boolean - Default:
true - Whether the sidebar has a button that collapses it to a rail of icons, from 640px up.
currentPath- Type:
string - The path of the page being shown, from your router (
useLocation().pathname, orusePathname()in Next.js). The link to that path, or else the longest link above it (/recipesfor/recipes/12), getsaria-current="page"and the current look. defaultCollapsed- Type:
boolean - Default:
false - Whether the sidebar starts collapsed, when it keeps its own state.
footer- Type:
ReactNode - Below the links: the signed-in person, an account
Menu. Clipped to the rail when collapsed. header- Type:
ReactNode - Above the links: your logo, product name or a workspace
Menu. Clipped to the rail when collapsed. label(required)- Type:
string - Names the navigation for screen readers ("Main"). It is not shown.
menuLabel- Type:
string - Default:
"Menu" - Below 640px: the name of the button that opens the sidebar as a drawer, and the drawer's title (shown when there is no
header). onCollapsedChange- Type:
((collapsed: boolean) => void) - Called with the new state when the person collapses or expands the sidebar.
ref- Type:
Ref<HTMLDivElement> - Reaches the rendered element.
sections(required)- Type:
readonly SidebarSection[] - The links, in groups:
sections={[{ items: [...] }, { label: "Library", items: [...] }]}.
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 sidebar issections,headerandfooter.isCollapsed: Not a prop. Ouro UI usescollapsedwithonCollapsedChange, ordefaultCollapsed.items: Not a prop. Links go insections:sections={[{ items: [{ label: "Recipes", href: "/recipes" }] }]}.pathname: Not a prop. Ouro UI usescurrentPath:<Sidebar currentPath={pathname}>.
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
SidebarButtonItem
A button in a Sidebar, for an action rather than a page: "New chat", "Search" opening a Command.
interface SidebarButtonItem extends SidebarItemBase {
/** What the button does. In the phone's drawer, the drawer closes first. */
onClick: (event: MouseEvent<HTMLElement>) => void;
/** Not with `onClick` alone. Give a link `href`; a button has none. */
href?: never;
/** Not on a button. Only a link can be the current page. */
current?: never;
/** Not on a button. Nested links go in an item without `onClick`: `{ label: "Settings", items: [...] }`. */
items?: never;
}SidebarGroupItem
An item with links nested under it, one level deep: a button that shows and hides them. It starts open when it holds the current page. On the collapsed rail the button opens them in a Menu beside it.
interface SidebarGroupItem extends SidebarItemBase {
/** The nested links and buttons. They do not nest further. */
items: readonly (SidebarLinkItem | SidebarButtonItem)[];
/** Not with `items`. To link the group's own page, make it the group's first item. */
href?: never;
/** Not with `items`; the group's button shows and hides them. */
onClick?: never;
/** Not on a group. One of its links is the current page. */
current?: never;
}SidebarItem
One item of a Sidebar: a link (href), a button (onClick), or links nested under a button (items).
type SidebarItem = SidebarLinkItem | SidebarButtonItem | SidebarGroupItem;SidebarLinkItem
A link in a Sidebar.
interface SidebarLinkItem extends SidebarItemBase {
/** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
href: string;
/**
* Marks this link as the current page (`aria-current="page"`), when the Sidebar's
* `currentPath` cannot tell: a link whose page lives at another path.
*/
current?: boolean | undefined;
/** Called when the link is followed, before your router navigates. */
onClick?: ((event: MouseEvent<HTMLElement>) => void) | undefined;
/** Not with `href`. Nested links go in an item without `href`: `{ label: "Settings", items: [...] }`. */
items?: never;
}SidebarSection
A group of links in a Sidebar, under an optional label.
interface SidebarSection {
/** The group's name, shown above its links: "Library". */
label?: string | undefined;
/** The group's items, in order: links, buttons and nested links. */
items: readonly SidebarItem[];
}