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 the link of the section it is in aria-current="true", both underlined in the primary colour, distinct from hover and press highlights. Built on Base UI's Navigation Menu.
On this site
This site's own pages use NavigationMenu too. Open one with X-ray pointing it out: The Nautilus's story · The home page
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.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-menu#v1.3.10Or, 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.3.10 ourostack/ouro-ui/navigation-menu-tests#v1.3.10import { 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 getsaria-current="page", in the row or in a panel; else the longest link above it, the section the page is in, getsaria-current="true", so a screen reader hears one current page. 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;
}
);