Navigation Menu
A full top-bar navigation pattern — logo, centre menu with mega-menu popups, and right-aligned action slots for language selector and auth CTAs. Ships two desktop trigger treatments (`pill`, `underline`), a `mode="mobile"` variant with a right-side drawer + submenu drill-in, richer mega-menu rows (tag + badge), and a theme-aware logo that swaps between light and dark sources. Built on @base-ui/react with 5 sizes, 5 rounded options, and LTR/RTL support.
Playground
Installation
pnpm add @tessinaui/uiUsage
Minimal menu
import {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuContent,
NavigationMenuLink,
NavigationMenuItemLink,
} from "@tessinaui/ui";<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Solutions</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid grid-cols-3 gap-2 min-w-[40rem]">
<NavigationMenuLink
href="/ecommerce"
icon={<ShoppingCart />}
title="Ecommerce"
description="Online stores"
/>
{/* ... */}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuItemLink href="/pricing">Pricing</NavigationMenuItemLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>Full mega menu (Deel-style)
Use NavigationMenuBar to lay out a logo, centred list, and actions. Combine
NavigationMenuColumns, NavigationMenuSectionHeading, NavigationMenuFeatured
and NavigationMenuPopupFooter inside NavigationMenuContent for
rich popups with a side-pane and quick-link footer row.
Inside the actions slot, use NavigationMenuAction (a Button wrapper that
inherits the menu's size + rounded) and NavigationMenuLanguageSelector
with NavigationMenuLanguageItem children for the locale dropdown. Every
action-row element stays visually aligned with the triggers at any size.
<NavigationMenu fullWidth>
<NavigationMenuBar
logo={
<NavigationMenuLogo href="/">
<Sparkles /> <span>Tessina</span>
</NavigationMenuLogo>
}
actions={
<NavigationMenuActions>
<NavigationMenuLanguageSelector flag={<Globe2 />} code="EN">
<NavigationMenuLanguageItem code="EN" active>English</NavigationMenuLanguageItem>
<NavigationMenuLanguageItem code="ES">Español</NavigationMenuLanguageItem>
<NavigationMenuLanguageItem code="FR">Français</NavigationMenuLanguageItem>
</NavigationMenuLanguageSelector>
<NavigationMenuAction variant="ghost">Log in</NavigationMenuAction>
<NavigationMenuAction>Book a demo</NavigationMenuAction>
</NavigationMenuActions>
}
>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Use cases</NavigationMenuTrigger>
<NavigationMenuContent padding="none">
<div className="flex flex-col min-w-[44rem]">
<div className="flex gap-6 p-6">
<div className="flex-1">
<NavigationMenuSectionHeading>How we help</NavigationMenuSectionHeading>
<NavigationMenuColumns cols={2}>
<NavigationMenuLink href="#" icon={<ShoppingCart />} title="Ecommerce" description="Online stores" />
{/* ... */}
</NavigationMenuColumns>
</div>
<div className="w-60 shrink-0">
<NavigationMenuFeatured
href="/customers"
title="Customer Stories"
description="See how teams scale with Tessina"
imageSrc="/hero.jpg"
ctaLabel="See all stories"
/>
</div>
</div>
<NavigationMenuPopupFooter>
<a href="#">Book a demo</a>
<a href="#">Compare Tessina</a>
<a href="#">Help center</a>
</NavigationMenuPopupFooter>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuItemLink href="/pricing">Pricing</NavigationMenuItemLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenuBar>
</NavigationMenu>Mobile variant
Pass mode="mobile" on the root to render the compact mobile flow: a 64px bar
with logo + primary CTA + hamburger, opening a right-side drawer that stacks
the L1 items as full-width rows with trailing chevrons. Tapping an item with
a submenu drills into a vertically-stacked panel with a back-arrow header.
Compose with these new parts:
NavigationMenuMobileTrigger— hamburger / close toggleNavigationMenuMobileDrawer— hosts the drawer, header slots, panel stage, and footerNavigationMenuMobileHeader— the drawer's top row (logo + CTA on L1; back-arrow + section label + CTA on submenu)NavigationMenuBackTrigger— back-arrow that pops the submenu stackNavigationMenuMobileSection— eyebrow heading + divider for submenu contentNavigationMenuMobilePanel— wraps submenu content; shown only when itsvaluematches the active itemNavigationMenuMobileFooter— beige band at the bottom with quick-links + language slot
On mobile, the regular NavigationMenuList / NavigationMenuItem /
NavigationMenuTrigger tree still represents your L1 structure; the Trigger
and Content stop rendering their desktop UI and become declarations the
parent Item reads to build the row. Mobile submenu content lives in
NavigationMenuMobilePanels passed to the drawer's panels slot, matched by
value.
<NavigationMenu mode="mobile" size="md">
<NavigationMenuBar
logo={
<NavigationMenuLogo
href="/"
lightSrc="/logo-black.svg"
darkSrc="/logo-white.svg"
/>
}
actions={
<NavigationMenuActions>
<NavigationMenuAction size="sm">Book a demo</NavigationMenuAction>
<NavigationMenuMobileTrigger />
</NavigationMenuActions>
}
/>
<NavigationMenuMobileDrawer
rootHeader={
<NavigationMenuMobileHeader>
<NavigationMenuLogo href="/" lightSrc="/logo-black.svg" darkSrc="/logo-white.svg" />
<div className="ms-auto flex items-center gap-2">
<NavigationMenuAction size="sm">Book a demo</NavigationMenuAction>
<NavigationMenuMobileTrigger />
</div>
</NavigationMenuMobileHeader>
}
submenuHeader={(active) => (
<NavigationMenuMobileHeader>
<NavigationMenuBackTrigger label={labelFor(active)} />
<div className="ms-auto flex items-center gap-2">
<NavigationMenuAction size="sm">Book a demo</NavigationMenuAction>
<NavigationMenuMobileTrigger />
</div>
</NavigationMenuMobileHeader>
)}
footer={
<NavigationMenuMobileFooter
language={<NavigationMenuLanguageSelector flag={<Globe2 />} code="EN" size="sm" />}
>
<a href="#">Help center</a>
<span aria-hidden="true">·</span>
<a href="#">Talk to sales</a>
</NavigationMenuMobileFooter>
}
panels={
<NavigationMenuMobilePanel value="solutions">
<NavigationMenuMobileSection label="WHAT WE OFFER">
<NavigationMenuLink href="#" icon={<Building2 />} title="Enterprise" description="Scale globally" />
<NavigationMenuLink href="#" icon={<Zap />} title="Integrations" description="Connect your stack" />
</NavigationMenuMobileSection>
</NavigationMenuMobilePanel>
}
>
<NavigationMenuList>
<NavigationMenuItem value="solutions">
<NavigationMenuTrigger>Solutions</NavigationMenuTrigger>
<NavigationMenuContent />
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuItemLink href="/pricing">Pricing</NavigationMenuItemLink>
</NavigationMenuItem>
</NavigationMenuList>
<div className="px-6 pb-6 pt-2">
<NavigationMenuAction variant="outline" className="w-full">Log in</NavigationMenuAction>
</div>
</NavigationMenuMobileDrawer>
</NavigationMenu>Notes:
NavigationMenuItem value="…"is required for drill-in items; the value links the L1 row to itsNavigationMenuMobilePanel.NavigationMenuContentself-closes on mobile — it exists only so the parent Item detects the item has a submenu (adds the trailing chevron).- The
submenuHeaderrender-prop receives the active item'svalueso you can look up its display label for the back-arrow. - The drawer slides in from the inline-end edge (
rightin LTR,leftin RTL) and the back-arrow icon mirrors automatically.
Showcase
Common layouts — icon-tile grids, description grids, mixed triggers, sizes, and RTL:
API Reference
NavigationMenu
The root of the menu. Wraps the <nav> element and auto-renders the full Portal → Positioner → Popup → Viewport chain. Pass size, rounded, intent, and dir here — they propagate automatically to all child parts via React context.
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "desktop" | "mobile" | "desktop" | Layout mode. desktop renders the horizontal trigger-popup bar; mobile renders a compact bar + right-side drawer flow |
variant | "pill" | "underline" | "pill" | Desktop trigger treatment. pill fills the active trigger with bg-secondary; underline keeps the trigger transparent and animates a 2px primary bar under the label when the popup opens |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Controls trigger height, padding, typography, and gap |
rounded | "none" | "sm" | "md" | "lg" | "full" | "lg" | Corner radius of the popup surface |
intent | "none" | "error" | "none" | Focus-ring colour |
orientation | "horizontal" | "vertical" | "horizontal" | Menu orientation. Only horizontal is styled in v1 |
dir | "ltr" | "rtl" | "ltr" | Text direction — flips keyboard-nav, arrow-key mapping, and popup side |
value | string | null | — | Controlled open item value |
defaultValue | string | null | null | Uncontrolled default open value |
onValueChange | (value, details) => void | — | Fires when the open item changes |
delay | number | 50 | Open delay on hover (ms) |
closeDelay | number | 150 | Close delay after leaving the popup (ms) |
modal | boolean | false | Render a backdrop behind the popup |
sideOffset | number | 8 | Distance between trigger and popup (px) |
fullWidth | boolean | false | Stretch the root <nav> to 100% width — useful when composing with NavigationMenuBar |
NavigationMenuBar
A 3-slot top-bar layout. Slots the logo inline-start, renders children (your
NavigationMenuList) in the centre, and slots the actions (language selector,
auth CTAs) inline-end. Height and padding scale with the root size.
| Prop | Type | Default | Description |
|---|---|---|---|
logo | React.ReactNode | — | Inline-start slot. Typically a NavigationMenuLogo |
actions | React.ReactNode | — | Inline-end slot. Typically a NavigationMenuActions |
align | "between" | "center" | "start" | "between" | How to position the centre list |
NavigationMenuLogo
A styled anchor wrapper for your product logo. Applies size-aware typography and SVG sizing, and optionally swaps the image between a light-theme and dark-theme source.
Pass lightSrc and darkSrc to enable automatic theme-aware swap — the
component observes <html> for a .dark class or data-theme="dark"
attribute (compatible with next-themes, fumadocs-ui, and most hand-rolled
theme providers). No hard dependency on next-themes. If only one source is
provided, or only src, the component renders it as-is. Passing children
bypasses image rendering entirely (full escape hatch).
<NavigationMenuLogo
href="/"
lightSrc="/logo-black.svg"
darkSrc="/logo-white.svg"
alt="Tessina"
/>| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Destination (usually "/") |
src | string | — | Single logo source. Used for both themes (no swap) |
lightSrc | string | — | Logo shown in light theme. Pair with darkSrc for theme swap |
darkSrc | string | — | Logo shown in dark theme. Pair with lightSrc for theme swap |
alt | string | "Logo" | Alt text for the image |
width | number | string | — | Passed to the underlying <img> |
height | number | string | — | Passed to the underlying <img> |
children | React.ReactNode | — | Custom content — overrides all src / lightSrc / darkSrc props |
NavigationMenuActions
A right-side flex container for action buttons. Applies size-aware gap.
NavigationMenuAction
A Button wrapper that inherits size and rounded from the root
NavigationMenu via React context, so every action in NavigationMenuActions
renders at the same height and matching corners as the triggers. Accepts every
other Button prop (variant, intent, loading, leadingIcon, etc.) —
size and rounded can still be passed explicitly to override the context.
<NavigationMenuActions>
<NavigationMenuAction variant="ghost">Log in</NavigationMenuAction>
<NavigationMenuAction>Book a demo</NavigationMenuAction>
</NavigationMenuActions>| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | inherited from NavigationMenu | Overrides the root size |
rounded | "none" | "sm" | "md" | "lg" | "full" | inherited from NavigationMenu | Overrides the root rounded |
…ButtonProps | All other Button props (variant, intent, loading, leadingIcon, etc.) |
NavigationMenuLanguageSelector
A pill-shaped button for language / locale switching. Shows a flag, a short
code (e.g. "EN"), and a trailing chevron. By default inherits size and
rounded from the root NavigationMenu, so it's always visually aligned with
the other action-row buttons.
Pass NavigationMenuLanguageItem children to turn it into a dropdown trigger
— the component will open a Base UI menu on click, identical to the prefix
dropdown pattern used in Field. Without children it renders as a plain
button and fires onClick.
<NavigationMenuLanguageSelector flag={<Globe2 />} code="EN">
<NavigationMenuLanguageItem code="EN" active>English</NavigationMenuLanguageItem>
<NavigationMenuLanguageItem code="ES">Español</NavigationMenuLanguageItem>
<NavigationMenuLanguageItem code="FR">Français</NavigationMenuLanguageItem>
</NavigationMenuLanguageSelector>Pass iconOnly to swap the pill for a compact square globe button — ideal for
the underline variant where the right cluster stays visually light. The
dropdown popup still lists flags + codes + language names when opened.
<NavigationMenuLanguageSelector
iconOnly
iconLabel="Change language"
flag={<Globe2 />}
code="EN"
>
<NavigationMenuLanguageItem code="EN" active>English</NavigationMenuLanguageItem>
<NavigationMenuLanguageItem code="ES">Español</NavigationMenuLanguageItem>
</NavigationMenuLanguageSelector>| Prop | Type | Default | Description |
|---|---|---|---|
flag | React.ReactNode | — | Emoji, <img>, or SVG |
code | string | — | Short uppercase code, e.g. "EN" |
showChevron | boolean | true | Whether to render the trailing chevron |
iconOnly | boolean | false | Render only a globe icon in a square button — hides the flag + code + chevron |
iconLabel | string | "Change language" | ARIA label applied when iconOnly is true |
size | "xs" | "sm" | "md" | "lg" | "xl" | inherited from NavigationMenu | Overrides the root size |
rounded | "none" | "sm" | "md" | "lg" | "full" | inherited from NavigationMenu | Overrides the root rounded |
children | React.ReactNode | — | NavigationMenuLanguageItem children turn the selector into a dropdown |
NavigationMenuLanguageItem
A menu item for use inside NavigationMenuLanguageSelector. Supports a flag, a
short code column, and the language name as the primary label.
| Prop | Type | Default | Description |
|---|---|---|---|
flag | React.ReactNode | — | Emoji, <img>, or SVG |
code | string | — | Short uppercase code shown in a muted column |
active | boolean | false | Marks the item as the current language |
…Menu.ItemProps | All Base UI Menu.Item props including onClick / disabled |
NavigationMenuList
Thin wrapper over Base UI List — renders a <ul>. Handles flex layout and size-appropriate gap.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction |
NavigationMenuItem
Wraps Base UI Item — renders a <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique identifier. Auto-generated if omitted |
NavigationMenuTrigger
A button that opens its associated popup on hover or click. Auto-renders a rotating chevron.
| Prop | Type | Default | Description |
|---|---|---|---|
leadingIcon | React.ReactNode | — | Optional icon before the label |
showChevron | boolean | true | Whether to render the trailing chevron |
rounded | "none" | "sm" | "md" | "lg" | "full" | "full" | Corner rounding of the trigger pill |
The active trigger (popup open) fills with bg-secondary when the root variant is "pill" (default) and animates a 2px bg-primary underline under the label when variant="underline". Override with className for a branded accent.
NavigationMenuItemLink
A plain top-level link (no popup). Use for items like "Pricing" or "Docs" that sit next to dropdown triggers.
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Link destination |
active | boolean | false | Marks as the current page — applies a filled state |
leadingIcon | React.ReactNode | — | Optional icon before the label |
rounded | "none" | "sm" | "md" | "lg" | "full" | "full" | Corner rounding |
NavigationMenuContent
A container for popup content. Compose your own grid inside — no layout opinions.
| Prop | Type | Default | Description |
|---|---|---|---|
padding | "default" | "none" | "default" | Set "none" to remove inner padding — useful when you render your own layout with a flush NavigationMenuPopupFooter |
keepMounted | boolean | false | Keep the content in the DOM while the popup is closed |
The element exposes data-activation-direction="left" \| "right" \| "up" \| "down" so you can customise the morph animation.
NavigationMenuSectionHeading
A small uppercase eyebrow label (e.g. "HOW WE HELP", "WHAT WE OFFER") used to
group links inside a popup. Renders an <h4> with size-aware typography and
tracking-wider.
NavigationMenuColumns
A CSS-grid wrapper for the link grid. Responsive: 1 column on mobile, cols
columns at sm+.
| Prop | Type | Default | Description |
|---|---|---|---|
cols | 1 | 2 | 3 | 4 | 2 | Number of columns from sm up |
NavigationMenuContentHeader
An optional header row that sits above a NavigationMenuColumns grid inside a
NavigationMenuContent. Renders a small uppercase section label with an
optional count on the inline-start and a “See all →” action on the
inline-end. Useful when the popup is a product catalogue and authors want to
echo the section name + link out to a full index page.
<NavigationMenuContent padding="none">
<div className="flex min-w-[48rem] flex-col p-4">
<NavigationMenuContentHeader
label="Products"
count={8}
action={{ label: "See all", href: "/products" }}
/>
<NavigationMenuColumns cols={2}>
{/* ...links */}
</NavigationMenuColumns>
</div>
</NavigationMenuContent>| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | Required. Small uppercase section label (e.g. "Products") |
count | number | — | Optional count rendered after a middle-dot separator (Products · 8) |
action | { label: React.ReactNode; href?: string; onClick?: () => void } | — | Optional trailing action — renders as a primary-coloured link that ends in an arrow (flips in RTL) |
divider | boolean | true | Render a bottom border beneath the header row |
NavigationMenuFeatured
A featured side-pane card with a hero image, title, description, and an
animated CTA arrow. Use it as a grid child next to a NavigationMenuColumns
link grid.
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Required. Destination for the card + CTA |
title | React.ReactNode | — | Required. Primary label |
description | React.ReactNode | — | Supporting text |
imageSrc | string | — | Hero image URL |
imageAlt | string | "" | Alt text |
ctaLabel | React.ReactNode | "Learn more" | CTA link label |
The arrow mirrors automatically in RTL.
NavigationMenuPopupFooter
A footer row pinned to the bottom of a popup. Two shapes:
variant="bar"(default) — thin horizontal row of quick-links + optional close button. Typically rendered insideNavigationMenuContentwithpadding="none"so the row sits flush against the popup edge.variant="promo"— soft-tinted promo card with a title + description on the inline-start and an action slot on the inline-end. Useful for cross-sells like “Paperloom for teams” at the bottom of a product popup.
{/* Bar (default) — quick-link row */}
<NavigationMenuPopupFooter>
<a href="#">Book a demo</a>
<span aria-hidden>·</span>
<a href="#">Help center</a>
</NavigationMenuPopupFooter>
{/* Promo — title + description + CTA */}
<NavigationMenuPopupFooter
variant="promo"
title="Paperloom for teams"
description="Unify product, design, and engineering in one workspace."
action={<Button variant="ghost" size="sm">Explore →</Button>}
/>| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bar" | "promo" | "bar" | Footer shape. bar renders an action row with children + optional close; promo renders a soft-tinted card with title / description / action |
showClose | boolean | true | (bar only) Whether to render the built-in NavigationMenuClose button |
closeLabel | string | "Close menu" | (bar only) ARIA label for the close button |
title | React.ReactNode | — | (promo only) Card headline |
description | React.ReactNode | — | (promo only) Supporting copy below the title |
action | React.ReactNode | — | (promo only) Inline-end action — typically a <Button variant="ghost"> |
NavigationMenuClose
A close button that dismisses the currently-open popup. Uses NavigationMenu
context to update the open value — works for both controlled and uncontrolled
roots. By default renders a Lucide X icon; pass children to override.
NavigationMenuLink (grid-cell helper)
The canonical popup-cell primitive. Covers both Swap-style (icon tile + label) and Linear-style (title + description) layouts via the iconBackground toggle. Pair with tag / badge for richer product-card rows and a “Soon”-style preview state.
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Required. Link destination |
title | React.ReactNode | — | Required. Primary label |
description | React.ReactNode | — | Secondary descriptive text |
icon | React.ReactNode | — | Optional leading icon |
iconBackground | boolean | true | Render the icon in a filled bg-secondary tile |
tag | React.ReactNode | — | Small uppercase accent label rendered at the end of the title row (e.g. "Canvas", "Workflow"). Uses text-muted-foreground |
badge | { label: string; tone?: "primary" | "info" | "success" | "warning" | "error" | "neutral" } | — | Optional pill-style badge rendered at the end of the title row for preview / beta / coming-soon rows. Tone maps to the intent tokens (bg-primary-light text-primary, etc.) |
active | boolean | false | Marks this link as the current page |
<NavigationMenuLink
href="/canvas"
icon={<Palette />}
title="Canvas"
tag="Design"
description="Collaborative canvas for product thinking"
/>
<NavigationMenuLink
href="/agents"
icon={<Wand2 />}
title="Agents"
badge={{ label: "Soon", tone: "primary" }}
description="Autonomous agents scoped to your data"
/>Size Variants
All five sizes scale trigger height, padding, text size, chevron size, and popup padding proportionally. Pass size once to the root NavigationMenu — all descendants inherit via context:
<NavigationMenu size="lg">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Solutions</NavigationMenuTrigger>
{/* ... */}
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>| Size | Trigger height | Typography |
|---|---|---|
xs | h-8 | text-sm |
sm | h-10 | text-sm |
md | h-12 | text-sm |
lg | h-14 | text-base |
xl | h-24 | text-2xl |
Popup Rounding
<NavigationMenu rounded="sm">...</NavigationMenu>
<NavigationMenu rounded="md">...</NavigationMenu>
<NavigationMenu rounded="lg">...</NavigationMenu>
<NavigationMenu rounded="full">...</NavigationMenu>rounded applies to the popup surface only — triggers render as pills regardless, controlled by their own rounded prop (defaults to "full").
Pill vs. Underline Triggers
The root variant prop switches the desktop trigger treatment. Both shapes
share the same API — every other part (popup, link grid, footer, language
selector, actions) renders identically.
{/* Pill — default. Active trigger fills with bg-secondary */}
<NavigationMenu variant="pill">…</NavigationMenu>
{/* Underline — transparent trigger with a 2px primary bar under the label */}
<NavigationMenu variant="underline">…</NavigationMenu>When to use which:
pillis the safe default — the filled active state is a strong open-popup signal and works well against dense bars with lots of actions. Best for product app chrome (dashboards, tools, admin).underlineis lighter and more editorial — inactive triggers sit intext-muted-foregroundand the primary underline quietly animates in when a popup opens. Best for marketing sites and content-heavy product pages where the bar should read as calm and recede into the page.
NavigationMenuItemLink inherits the variant too — plain top-level links
(like “Pricing”) get the same active underline when active is
set, so pill + underline bars always feel internally consistent.
Swap-style vs. Linear-style Links
Use the iconBackground prop on NavigationMenuLink to toggle between the two common popup styles:
{/* Swap-style — icon tile + label */}
<NavigationMenuLink
href="/ecommerce"
icon={<ShoppingCart />}
title="Ecommerce"
description="Online stores"
iconBackground // default
/>
{/* Linear-style — no tile, title + description only */}
<NavigationMenuLink
href="/about"
title="About"
description="Meet the team"
iconBackground={false}
/>RTL
Pass dir="rtl" on the root. The <nav> element gets dir="rtl" and Base UI + Floating UI handle logical side resolution automatically:
<NavigationMenu dir="rtl">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>الحلول</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink
href="#"
icon={<ShoppingCart />}
title="التجارة الإلكترونية"
description="متاجر عبر الإنترنت"
/>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>The trigger chevron always points down regardless of direction — it's an openness affordance, not a directional arrow.
Keyboard
| Key | Action |
|---|---|
Tab | Focus the first trigger |
ArrowRight / ArrowLeft | Move between triggers (flips in RTL) |
ArrowDown / Enter / Space | Open the focused trigger's popup and focus the first link |
Escape | Close the popup and return focus to the trigger |
Tab inside popup | Move focus through popup links; Shift+Tab reverses |
Accessibility
- Built on
@base-ui/react/navigation-menu— follows the WAI-ARIA Disclosure pattern for menu triggers with popups - Triggers are
[role="button"]with[aria-expanded]state - Popup panels are
[role="menu"]-adjacent; each link is a standard<a>for native link semantics (search-engine-friendly + assistive-tech-friendly) - Focus management: opening a popup moves focus to the first link; closing returns focus to the trigger
- Color contrast on trigger and link hover/active states meets WCAG 2.1 AA
- Animations respect
prefers-reduced-motion— Tessina components rely ontransition-*classes that native media queries disable automatically
Mobile parts
All parts below are only relevant when the root has mode="mobile". They are
safe to import unconditionally — on desktop they simply don't render.
NavigationMenuMobileTrigger
The hamburger / close toggle button. Reads the drawer's open state from
context and crossfades between Menu and X icons.
| Prop | Type | Default | Description |
|---|---|---|---|
openLabel | string | "Open menu" | ARIA label when the drawer is closed |
closeLabel | string | "Close menu" | ARIA label when the drawer is open |
NavigationMenuMobileDrawer
Hosts the drawer surface (Base UI Dialog portal) and the stage that swaps
between the L1 list and the active submenu panel. Authors pass the L1 list as
children and any submenu content via the panels slot.
| Prop | Type | Default | Description |
|---|---|---|---|
rootHeader | React.ReactNode | — | Header shown when on the L1 list |
submenuHeader | (activeItem: string) => React.ReactNode | — | Header shown when a submenu is active |
footer | React.ReactNode | — | Rendered below the L1 list — typically a NavigationMenuMobileFooter |
panels | React.ReactNode | — | Submenu panels — typically NavigationMenuMobilePanel wrappers |
title | string | "Navigation" | <Dialog> ARIA title (visually hidden) |
description | string | — | Optional <Dialog> description (visually hidden) |
The drawer uses @base-ui/react/dialog under the hood — focus is trapped,
Escape closes, and focus is returned to the NavigationMenuMobileTrigger
that opened it.
NavigationMenuMobileHeader
A simple 64px-tall flex row with a bottom border, used inside
rootHeader / submenuHeader slots. No props beyond standard <div>.
NavigationMenuBackTrigger
Back-arrow button that pops the submenu (sets activeItem back to null).
Renders an ArrowLeft icon that flips in RTL, followed by the section label
with a 2px underline accent.
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | Section label rendered next to the arrow |
ariaLabel | string | "Back" | ARIA label (used when no visible label is provided) |
NavigationMenuMobileSection
Optional eyebrow-heading + divider wrapper for submenu content.
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | Eyebrow heading — rendered uppercase above a divider |
NavigationMenuMobilePanel
Wraps a submenu's content. Renders only when its value matches the
currently-drilled item (when no panel matches, the stage shows the L1 list).
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. Must match the value of the drill-in NavigationMenuItem |
NavigationMenuMobileFooter
Beige footer band at the bottom of the L1 stage. Centers a horizontal quick-links row (children) and optionally a language row below it.
| Prop | Type | Default | Description |
|---|---|---|---|
language | React.ReactNode | — | Language row (typically a NavigationMenuLanguageSelector) rendered below children |
Mobile keyboard & a11y
| Key | Action |
|---|---|
Tab | Focus the NavigationMenuMobileTrigger |
Enter / Space on trigger | Open the drawer |
Tab inside drawer | Cycle through rows and interactive elements |
Enter on an L1 row | Drill into the matching submenu panel |
Enter on NavigationMenuBackTrigger | Pop back to the L1 list |
Escape | Close the drawer (focus returns to the mobile trigger) |
- The drawer uses Base UI
Dialog, so focus is trapped while open and scroll on the outer page is locked. - The L1 stage is
aria-hiddenwhen a submenu is active, and vice-versa, so screen-reader navigation stays in the visible stage. - Theme-aware logo swap observes
<html>for a.darkclass ordata-theme="dark"— no SSR flicker because the first paint is deterministic (lightSrcbefore hydration).
Out of Scope (v1)
- Full-page takeover navs (à la AE.1) — planned as a future
NavOverlaycomponent - Floating / pill-container style — the top-bar treatment is the only style in v1. A future
variant="floating"axis may add a rounded capsule container - Auto-responsive mode switching —
modeis explicit. A futuremode="responsive"with a breakpoint-driven auto-switcher is a follow-up - Nested submenus (3+ levels) — mobile mode supports exactly two levels (L1 → submenu panel). Deeper nesting is out of scope
Toolbar
Container for grouping a set of controls — buttons, toggles, links, inputs, separators. Wraps `@base-ui/react/toolbar` for keyboard focus management. Four variants, three sizes, five rounded values, horizontal (with overflow wrap) + vertical orientation, LTR/RTL.
Menubar
A desktop-app style horizontal menu bar (File / Edit / View / Help) with shared open state, hover-switch behaviour, and arrow-key navigation between top-level triggers.