Navigation
NavigationMenu
A row of links where some open a panel of further links, each with a line of description: the navigation of a site with more pages than fit in a bar. Panels open on hover or click, move between each other without closing, and close on Escape, on an outside press and when a link is chosen. The current page is marked aria-current="page" and a bar at the start edge, distinct from hover and press highlights. Built on Base UI's Navigation Menu.
In Spoonjoy
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/navigation-menu#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/navigation-menuIts 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-menu#v1.2.13 ourostack/ouro-ui/navigation-menu-tests#v1.2.13import { NavigationMenu } from "@/components/ouro/navigation-menu/navigation-menu";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<NavigationMenu
label="Main"
items={[
{
label: "Recipes",
links: [
{ label: "Browse", href: "/recipes", description: "Everything you have saved." },
{ label: "Import", href: "/recipes/import", description: "Bring one in from a link." },
],
},
{ label: "Pricing", href: "/pricing" },
]}
currentPath={pathname}
/>Usage
Use Menu for actions rather than pages, and Sidebar for a column of links.
Use instead when
- Use
Navbarinstead when every item is a plain link; it also collapses into a drawer on a phone, which this row does not.
Props
Type: NavigationMenuProps.
className- Type:
string - Layout only: margins, grid placement (
ms-4). Appearance classes warn in development. 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, getsaria-current="page", in the row or in a panel. items(required)- Type:
readonly NavigationMenuItem[] - The row's items, in order: links, and buttons that open a panel of links.
label(required)- Type:
string - Names the navigation for screen readers ("Main"). It is not shown.
ref- Type:
Ref<HTMLElement> - Reaches the rendered element.
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 menu is data:items={[{ label: "Recipes", links: [...] }, { label: "Pricing", href: "/pricing" }]}.links: Not a prop. Ouro UI usesitems; a panel's links go in an item'slinks.orientation: Not a prop. The menu is a row; for a column of links useSidebar.pathname: Not a prop. Ouro UI usescurrentPath:<NavigationMenu 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
NavigationMenuItem
One item of a NavigationMenu: a link (href), or a button that opens a panel (links, content).
type NavigationMenuItem = NavigationMenuLinkItem | NavigationMenuPanelItem;NavigationMenuLink
One link inside a NavigationMenu panel.
interface NavigationMenuLink {
/** The link's text: "Browse". */
label: string;
/** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
href: string;
/**
* A line under the label that says what is there: "Everything you have saved."
*/
description?: string | undefined;
/** Marks this link as the current page, when the menu's `currentPath` cannot tell. */
current?: boolean | undefined;
}NavigationMenuLinkItem
A NavigationMenu item that is a link to a page.
interface NavigationMenuLinkItem {
/** The link's text: "Pricing". */
label: string;
/** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
href: string;
/** Marks this link as the current page, when the menu's `currentPath` cannot tell. */
current?: boolean | undefined;
/** Not with `href`. An item is a link (`href`) or a panel (`links`, `content`). */
links?: never;
/** Not with `href`. An item is a link (`href`) or a panel (`links`, `content`). */
content?: never;
}NavigationMenuPanelItem
A NavigationMenu item that opens a panel: a list of links, with any content at its start, such as a featured Card beside a two-column grid of links. A panel needs one of the two.
type NavigationMenuPanelItem = NavigationMenuPanelBase &
(
| {
/**
* The links in the panel, each with an optional `description`.
*/
links: readonly NavigationMenuLink[];
/**
* Anything else in the panel, before its links (above them on a phone): a featured
* `Card`, an image, a line of text. A link in it closes the panel when it is followed.
*/
content?: ReactNode;
}
| {
links?: undefined;
/**
* The panel's content, when it has no list of links. A link in it closes the panel when it is followed.
*/
content: ReactNode;
}
);