Navigation Menu
A site navigation bar — brand, links, mega-menu panels and actions — that collapses to a mobile drawer built from the same tree. Solid, floating and overlay bars, scroll-aware chrome, priority+ overflow, and disclosure semantics rather than menu roles.
pnpm add @tessinaui/uiWhen to use
- A marketing or product site's top navigation: a brand, a handful of sections, some of which open panels, and a sign-up path.
- Anywhere the same navigation has to work on a phone. The mobile drawer is built from the tree you already wrote, so there is one source of links.
- Sections that need a description, not just a label. A panel can carry grouped links, icons, badges and a featured card.
- A bar that may outgrow its room.
NavigationMenuOverflowmoves the last links into a "More" menu one at a time instead of wrapping. - Links that go to pages. Triggers open panels of ordinary links, so the bar reads as site navigation, not as an application menu.
When not to use
- For vertical navigation. That is Sidebar.
- For an application menu bar (File, Edit, View) with menu semantics and roving focus. Use Menubar.
- For a single button that opens a list of actions. Use DropdownMenu.
- For a static app header with no panels. TopHeaderDesktop and TopHeaderMobile are lighter. See Boundaries below.
- For a long list of top-level sections. A bar with a handful of items fits;
past that, move sections into panels or use
NavigationMenuOverflow.
Installation
pnpm add @tessinaui/uiUsage
State the navigation once. The desktop bar renders it inline; the drawer renders the same subtree when the viewport is narrow.
import {
NavigationMenu,
NavigationMenuBar,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
NavigationMenuItemLink,
NavigationMenuPanels,
NavigationMenuMobileDrawer,
NavigationMenuMobileTrigger,
} from "@tessinaui/ui";
<NavigationMenu>
<NavigationMenuBar
logo={<NavigationMenuLogo href="/">Tessina</NavigationMenuLogo>}
actions={<NavigationMenuMobileTrigger label="Menu" />}
drawer={<NavigationMenuMobileDrawer title="Navigation" />}
>
<NavigationMenuList>
<NavigationMenuItem value="product">
<NavigationMenuTrigger>Product</NavigationMenuTrigger>
<NavigationMenuContent label="Product">
<NavigationMenuLink href="/cards" title="Corporate cards" />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuItemLink href="/pricing" active>Pricing</NavigationMenuItemLink>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuPanels />
</NavigationMenuBar>
</NavigationMenu>Two rules cover most mistakes:
draweris a slot, not a child.childrenis the navigation the drawer renders, so a drawer placed inside it would be part of the tree it reads.Panelsgoes insideBar. It mounts the shared popup the panels animate in, and it renders nothing at all in mobile mode.
Examples
Default
A bar with one mega-menu panel, plain links, a skip link and an actions cluster.
Bar variants
solid sits on the page, floating detaches into a frosted capsule, and
overlay is transparent over hero media until content scrolls under it.
Trigger variants
The open trigger takes a fill (pill) or a ribbon (underline). The indicator
glyph is a slot: a chevron by default, any node, or none.
Sizes
Alignment
Links beside the brand, optically centred on the viewport, or joined to the actions cluster.
Actions
The clusters that recur across the surveyed sites, plus in-bar utilities.
Mega menu
Column groups, headers, dividers and a panel footer. Moving between two panels of different sizes morphs the shared viewport rather than closing and reopening.
Compact panels
Narrow list panels, external-link markers, and group dividers inside a panel.
Link anatomy
Every shape a panel row takes: description, icon, icon tile, badge, external, current, disabled, and what happens to text nobody on the design side wrote.
Featured content
A tinted rail for a promo or case study, and card tiles beside a link column.
Panel footer
A quiet strip, or a tinted band that carries its own call to action.
Backdrop
Dim and blur the page behind an open panel.
Announcement strip
A strip above the bar, composed from the real Banner.
Sub bar
A second row under the main bar. It is a second navigation landmark, so it takes
a label.
Scroll behaviour
elevate adds a shadow once content is underneath; condense also steps the bar
down one height rung.
Overflow
More links than room: items move into a “More” menu one at a time, last first.
Mobile
The drawer, with drill-in panels, a back row, and the primary action left in the collapsed bar.
Mobile accordion
The same drawer with groups that expand in place, and pinned actions at the foot.
Write each group as a NavigationMenuMobileSection in the drawer, as this
example does. A Trigger + Content item does not become an accordion row yet:
the row does nothing and its links stay open.
Responsive
One tree, both shells. Resize across 768px.
RTL
The bar reads its direction from the page, so a right-to-left wrapper is all it needs. The names the menu writes itself (the mobile trigger, the drawer's title and back button, the overflow trigger, the skip link and the skeleton's status) switch to Arabic with it. For another right-to-left language, pass every entry through strings.
Controlled
On color
An overlay bar is transparent, so over a coloured Surface or a hero it inherits on-color: its logo, links and actions take the plate's ink. solid and floating paint their own bar and resolve default unless you pass tone. When content scrolls under it (scrollBehavior), an overlay bar on a plate lifts to the plate's own colour instead of a frosted glass, and a floating capsule on a plate paints opaque, so its links and focus rings keep their contrast.
Loading
API Reference
NavigationMenu
| Prop | Type | Default | Description |
|---|---|---|---|
variant | solid | floating | overlay | solid | How the bar sits on the page |
triggerVariant | pill | underline | pill | The open/active affordance |
size | xs | sm | md | lg | xl | md | Density |
rounded | none | sm | md | lg | full | lg | Corner treatment; cascades to panels and rows |
tone | default | on-color | inherited | Set when the bar sits on a coloured plate |
scrollBehavior | none | elevate | condense | none | Reaction to content scrolling under the bar |
mode | responsive | desktop | mobile | responsive | Which shell renders; an explicit value always wins |
mobileLayout | drill-in | accordion | drill-in | How mobile panels present |
value / defaultValue | string | null | null | The open item |
onValueChange | (value, details) => void | — | Fires when a desktop panel opens or closes; the mobile drawer keeps its own state |
delay / closeDelay | number | 150 / 250 | Hover intent, in ms |
strings | Partial<NavigationMenuStrings> | — | Overrides for the names the menu writes itself (the mobile trigger, the drawer title and back button, the overflow trigger, the skip link). The table follows the bar's direction, set or inherited: Arabic under right-to-left, English otherwise. A part's own prop still wins |
NavigationMenuBar
| Prop | Type | Default | Description |
|---|---|---|---|
logo | ReactNode | — | Brand slot |
actions | ReactNode | — | Trailing cluster |
drawer | ReactNode | — | The mobile drawer; a slot, never a child |
align | start | center | end | start | Where the links sit |
sticky | boolean | false | Stick to the top of the scroll container |
contained | boolean | true | Cap and centre the bar's content width |
NavigationMenuLink
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The row's headline (required) |
description | ReactNode | — | One muted line; clamps at two |
icon / iconTile | ReactNode / boolean | — | Leading glyph, optionally on a bordered plate |
badge | { label, intent } | — | Trailing badge; composes Badge |
external | boolean | false | Adds the arrow, opens a new tab |
active | boolean | false | Sets aria-current="page" |
disabled | boolean | false | Drops href, sets aria-disabled |
closeOnClick | boolean | true | Close the panel when the link is followed |
render | RenderProp | — | Replace the <a>, e.g. a router Link |
Other parts
NavigationMenuLogo · NavigationMenuActions · NavigationMenuList
(overflow) · NavigationMenuItem ·
NavigationMenuTrigger (leadingIcon, indicator) ·
NavigationMenuItemLink (active, disabled, render) ·
NavigationMenuContent (padding, label) · NavigationMenuColumns
(cols, dividers) · NavigationMenuSectionHeading (rule) ·
NavigationMenuFeatured (tinted, href, render, active, closeOnClick) ·
NavigationMenuPopupFooter
(variant) · NavigationMenuPanels (backdrop, sideOffset, side,
align) · NavigationMenuSubBar (label, asRegion) ·
NavigationMenuSkipLink (strings) · NavigationMenuOverflow (items, moreLabel) ·
NavigationMenuMobileTrigger (label, iconOnly) ·
NavigationMenuMobileDrawer (title, side, footer) ·
NavigationMenuMobileSection (label, defaultOpen) ·
NavigationMenuSkeleton (items, labels, showLogo, actions, label,
dir, strings).
The Base UI primitives are exported too — NavigationMenuPortal,
Positioner, Popup, Viewport, Backdrop, Arrow — for anyone who needs to
reach between the layers that Panels bundles.
Accessibility
This is the disclosure pattern, not the ARIA menu pattern. Triggers are
buttons with aria-expanded; panels contain ordinary links. Menu roles promise
a keyboard model that websites do not implement, so claiming them makes a nav
worse for a screen-reader user, not better — the position the WAI-ARIA APG
takes for site navigation.
activesetsaria-current="page", not just paint.disableddrops thehref, so the item leaves the tab order, and setsaria-disabled.- The mobile drawer's hidden pane is
inertas well asaria-hidden, and focus moves into the pane you drilled into and back out again. - Items
NavigationMenuOverflowhas clipped areinertandaria-hiddenas well as invisible, so each link is met once — in the row or in the “More” menu, never both. An item that sheds while focused hands focus to the trigger. NavigationMenuSubBaris a second<nav>landmark and requires alabel; it warns in development without one.- Ship
NavigationMenuSkipLinkas the first thing in the nav (WCAG 2.4.1). - Interactive targets clear 44px wherever the pointer may be a finger — keyed to the pointer, so tablets are covered, not just narrow viewports.
Keyboard
| Key | Result |
|---|---|
Tab / Shift+Tab | Move through the bar, then into an open panel |
Enter / Space | Open or close the focused trigger's panel |
Arrow keys | Move between triggers, and within an open panel |
Esc | Close the panel and return focus to its trigger |
Home / End | First / last item in the list |
Screen readers & AT matrix
| Technology | Status |
|---|---|
| VoiceOver (macOS, Safari) | Passed 2026-09-14 — "link, Skip to content", "link, Tessina, home"; the trigger reads "Product, collapsed, button", Space reads "Product, expanded, button", Tab enters the panel on "link, Corporate cards, Spend controls, built in", Escape returns to "Product, collapsed, button" |
| NVDA | Untested — no Windows machine available |
| JAWS | Untested — no Windows machine available |
Boundaries
- Migrating from
NavigationMenuV2: the preview names are gone — renameNavigationMenuV2*→NavigationMenu*and import from@tessinaui/ui/navigation-menu. The originalNavigationMenuit replaced has been removed; itsContentHeader,LanguageSelectorand hand-built mobile panels have no one-to-one equivalent, because the drawer is now built from the same tree as the bar. TopHeaderDesktopcomposes a nav into a static header. If its items need real panels, use this component instead of adding them there.TopHeaderMobileis an app-screen header (title, back, actions). This component's mobile shell is a site nav — a brand, a menu and a call to action. They are not interchangeable.
Behaviour
Hover intent. Panels open after 150ms and close after 250ms. Base UI defaults
to 50/50, which is trigger-happy across a wide bar — a pointer crossing three
triggers on its way to the fourth opens all three. Tune with delay and
closeDelay.
The panel morphs. All panels share one popup, so moving between triggers animates size and position instead of closing and reopening. The incoming panel slides in from the side you came from.
Condense is frozen while a panel is open. A bar that changed height mid-hover would drag the panel's anchor with it.
The hover bridge. An invisible strip spans the gap between bar and panel, so a pointer travelling to the panel never leaves the menu.
Notes
- Highlighted rows carry their state in the fill. A 2px highlight ring was shipped library-wide in September 2026 and removed the next day by product decision; do not reintroduce one here.
mode="responsive"resolves throughmatchMediaand reports desktop until mounted, so the server and the first client paint agree.- The overflow row is a wrapping list clamped to one line: whatever the browser
wraps onto line two is clipped and listed in the “More” menu, so nothing is
measured ahead of time and no width is reserved for the trigger. Clipped items
stay laid out, which is why shedding one can never un-shed it. The server
renders every item with the clamp already on; where
ResizeObserveris unavailable the clamp comes off and the full row stays. Eachchildrencall must return oneNavigationMenuItem— the row reads its list items in order. - Past the last collapse threshold the nav row scrolls rather than colliding
(
NavigationMenuList overflow="scroll", the default). Exactly one zone in the bar yields, and it is always the one holding that row — a logo and a cluster of buttons have no graceful smaller form, so they keep their natural width. The rail shows no scrollbar: reserving a track would change the bar's height the moment it overflowed, and a mask-image fade would cut the alpha of any label under it, which no contrast gate can see. The affordance is the item sliced by the edge. Passoverflow="clip"for a bar whose container guarantees the room;NavigationMenuOverflowsets it that way for itself, since shedding and scrolling should never both be running. - A featured card given an
hrefbecomes ONE link: the whole card is the target, it takes a hover fill and a focus ring, and any inner "read more" anchor must become plain text — an anchor inside an anchor is invalid and puts two tab stops on one destination. Withouthrefthe card stays a plaindivwith no affordances, which is right for a stat block or a quote that goes nowhere. The hover is a relativeforeground/5rather thanbg-secondary, because--mutedand--secondaryare the same colour in dark and a tinted card would otherwise have no hover at all. - The mobile drawer's header is fixed geometry: the title keeps its size and position between levels, and the close control never yields its slot to the back control. At depth the title moves right by the width of the reserved back slot — the pattern every surveyed app uses — and its text becomes the parent item's label.
- The same panel children fill a desktop panel and the 272px drawer, so size
them for both.
NavigationMenuContentand the drawer clamp their direct child to their own width: a fixed-width column (w-[52rem]) shrinks instead of being clipped on a tablet or scrolling the drawer sideways. A fixed grid track deeper inside is not clamped — let such a grid collapse to one column. - Force
mode="desktop"only where the bar is known to have room. On a phone the nav row shrinks to a scrolling sliver narrower than one item, and an actions cluster with nocompactstand-in covers the brand. Leavemodeatresponsive, the default, and phones get the drawer.