DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
BreadcrumbTabsPaginationStepperMenu ItemToolbarMenubarNavigation MenuAccordionCollapsibleCommandBottom NavTop Header MobileTop Header DesktopSidebar
AI agentsContributing
  1. Components
  2. Navigation Menu

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/ui

When 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. NavigationMenuOverflow moves 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

Terminal
pnpm add @tessinaui/ui

Usage

State the navigation once. The desktop bar renders it inline; the drawer renders the same subtree when the viewport is narrow.

example.tsx
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:

  • drawer is a slot, not a child. children is the navigation the drawer renders, so a drawer placed inside it would be part of the tree it reads.
  • Panels goes inside Bar. 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

PropTypeDefaultDescription
variantsolid | floating | overlaysolidHow the bar sits on the page
triggerVariantpill | underlinepillThe open/active affordance
sizexs | sm | md | lg | xlmdDensity
roundednone | sm | md | lg | fulllgCorner treatment; cascades to panels and rows
tonedefault | on-colorinheritedSet when the bar sits on a coloured plate
scrollBehaviornone | elevate | condensenoneReaction to content scrolling under the bar
moderesponsive | desktop | mobileresponsiveWhich shell renders; an explicit value always wins
mobileLayoutdrill-in | accordiondrill-inHow mobile panels present
value / defaultValuestring | nullnullThe open item
onValueChange(value, details) => void—Fires when a desktop panel opens or closes; the mobile drawer keeps its own state
delay / closeDelaynumber150 / 250Hover intent, in ms
stringsPartial<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

PropTypeDefaultDescription
logoReactNode—Brand slot
actionsReactNode—Trailing cluster
drawerReactNode—The mobile drawer; a slot, never a child
alignstart | center | endstartWhere the links sit
stickybooleanfalseStick to the top of the scroll container
containedbooleantrueCap and centre the bar's content width

NavigationMenuLink

PropTypeDefaultDescription
titleReactNode—The row's headline (required)
descriptionReactNode—One muted line; clamps at two
icon / iconTileReactNode / boolean—Leading glyph, optionally on a bordered plate
badge{ label, intent }—Trailing badge; composes Badge
externalbooleanfalseAdds the arrow, opens a new tab
activebooleanfalseSets aria-current="page"
disabledbooleanfalseDrops href, sets aria-disabled
closeOnClickbooleantrueClose the panel when the link is followed
renderRenderProp—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.

  • active sets aria-current="page", not just paint.
  • disabled drops the href, so the item leaves the tab order, and sets aria-disabled.
  • The mobile drawer's hidden pane is inert as well as aria-hidden, and focus moves into the pane you drilled into and back out again.
  • Items NavigationMenuOverflow has clipped are inert and aria-hidden as 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.
  • NavigationMenuSubBar is a second <nav> landmark and requires a label; it warns in development without one.
  • Ship NavigationMenuSkipLink as 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

KeyResult
Tab / Shift+TabMove through the bar, then into an open panel
Enter / SpaceOpen or close the focused trigger's panel
Arrow keysMove between triggers, and within an open panel
EscClose the panel and return focus to its trigger
Home / EndFirst / last item in the list

Screen readers & AT matrix

TechnologyStatus
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"
NVDAUntested — no Windows machine available
JAWSUntested — no Windows machine available

Boundaries

  • Migrating from NavigationMenuV2: the preview names are gone — rename NavigationMenuV2* → NavigationMenu* and import from @tessinaui/ui/navigation-menu. The original NavigationMenu it replaced has been removed; its ContentHeader, LanguageSelector and hand-built mobile panels have no one-to-one equivalent, because the drawer is now built from the same tree as the bar.
  • TopHeaderDesktop composes a nav into a static header. If its items need real panels, use this component instead of adding them there.
  • TopHeaderMobile is 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 through matchMedia and 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 ResizeObserver is unavailable the clamp comes off and the full row stays. Each children call must return one NavigationMenuItem — 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. Pass overflow="clip" for a bar whose container guarantees the room; NavigationMenuOverflow sets it that way for itself, since shedding and scrolling should never both be running.
  • A featured card given an href becomes 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. Without href the card stays a plain div with no affordances, which is right for a stat block or a quote that goes nowhere. The hover is a relative foreground/5 rather than bg-secondary, because --muted and --secondary are 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. NavigationMenuContent and 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 no compact stand-in covers the brand. Leave mode at responsive, the default, and phones get the drawer.
MenubarAccordion

On this page

When to useWhen not to useInstallationUsageExamplesDefaultBar variantsTrigger variantsSizesAlignmentActionsMega menuCompact panelsLink anatomyFeatured contentPanel footerBackdropAnnouncement stripSub barScroll behaviourOverflowMobileMobile accordionResponsiveRTLControlledOn colorLoadingAPI ReferenceNavigationMenuNavigationMenuBarNavigationMenuLinkOther partsAccessibilityKeyboardScreen readers & AT matrixBoundariesBehaviourNotes