Tessina UI
Tessina UI
GitHubIntroduction
InstallationUsageTheming
Components
ButtonIconButtonLinkSpinnerBadgeAvatarStatusChipShortcutSkeletonSurfaceProgressMeterRating
LabelFieldFieldsetCheckboxRadioSwitchSliderSelectComboboxSearchTextareaNumberFieldDate PickerTime PickerOTP InputFile UploadCalendar
AlertBannerToastTooltip
TabsAccordionCollapsibleBreadcrumbPaginationStepperSegmentedControlButtonGroupToggleButtonToggleGroupToolbarNavigation MenuMenubarBottom NavSidebar
Split ButtonFABDropdown MenuContextMenuCommandPopoverHoverCard
ContainerStackFlexGridAspectRatioSpacerCardCarouselDividerScroll Area
Table
ChatBubblePromptInputCodeBlock
ModalAlertDialogDrawerAction SheetTop Header MobileTop Header DesktopEmptyStateForm
Contributing
ComponentsNavigation Blocks

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

Usage

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 toggle
  • NavigationMenuMobileDrawer — hosts the drawer, header slots, panel stage, and footer
  • NavigationMenuMobileHeader — the drawer's top row (logo + CTA on L1; back-arrow + section label + CTA on submenu)
  • NavigationMenuBackTrigger — back-arrow that pops the submenu stack
  • NavigationMenuMobileSection — eyebrow heading + divider for submenu content
  • NavigationMenuMobilePanel — wraps submenu content; shown only when its value matches the active item
  • NavigationMenuMobileFooter — 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 its NavigationMenuMobilePanel.
  • NavigationMenuContent self-closes on mobile — it exists only so the parent Item detects the item has a submenu (adds the trailing chevron).
  • The submenuHeader render-prop receives the active item's value so you can look up its display label for the back-arrow.
  • The drawer slides in from the inline-end edge (right in LTR, left in 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.

PropTypeDefaultDescription
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
valuestring | null—Controlled open item value
defaultValuestring | nullnullUncontrolled default open value
onValueChange(value, details) => void—Fires when the open item changes
delaynumber50Open delay on hover (ms)
closeDelaynumber150Close delay after leaving the popup (ms)
modalbooleanfalseRender a backdrop behind the popup
sideOffsetnumber8Distance between trigger and popup (px)
fullWidthbooleanfalseStretch 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.

PropTypeDefaultDescription
logoReact.ReactNode—Inline-start slot. Typically a NavigationMenuLogo
actionsReact.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"
/>
PropTypeDefaultDescription
hrefstring—Destination (usually "/")
srcstring—Single logo source. Used for both themes (no swap)
lightSrcstring—Logo shown in light theme. Pair with darkSrc for theme swap
darkSrcstring—Logo shown in dark theme. Pair with lightSrc for theme swap
altstring"Logo"Alt text for the image
widthnumber | string—Passed to the underlying <img>
heightnumber | string—Passed to the underlying <img>
childrenReact.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>
PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl"inherited from NavigationMenuOverrides the root size
rounded"none" | "sm" | "md" | "lg" | "full"inherited from NavigationMenuOverrides the root rounded
…ButtonPropsAll 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>
PropTypeDefaultDescription
flagReact.ReactNode—Emoji, <img>, or SVG
codestring—Short uppercase code, e.g. "EN"
showChevronbooleantrueWhether to render the trailing chevron
iconOnlybooleanfalseRender only a globe icon in a square button — hides the flag + code + chevron
iconLabelstring"Change language"ARIA label applied when iconOnly is true
size"xs" | "sm" | "md" | "lg" | "xl"inherited from NavigationMenuOverrides the root size
rounded"none" | "sm" | "md" | "lg" | "full"inherited from NavigationMenuOverrides the root rounded
childrenReact.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.

PropTypeDefaultDescription
flagReact.ReactNode—Emoji, <img>, or SVG
codestring—Short uppercase code shown in a muted column
activebooleanfalseMarks the item as the current language
…Menu.ItemPropsAll 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.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Layout direction

NavigationMenuItem

Wraps Base UI Item — renders a <li>.

PropTypeDefaultDescription
valuestring—Unique identifier. Auto-generated if omitted

NavigationMenuTrigger

A button that opens its associated popup on hover or click. Auto-renders a rotating chevron.

PropTypeDefaultDescription
leadingIconReact.ReactNode—Optional icon before the label
showChevronbooleantrueWhether 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.

PropTypeDefaultDescription
hrefstring—Link destination
activebooleanfalseMarks as the current page — applies a filled state
leadingIconReact.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.

PropTypeDefaultDescription
padding"default" | "none""default"Set "none" to remove inner padding — useful when you render your own layout with a flush NavigationMenuPopupFooter
keepMountedbooleanfalseKeep 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+.

PropTypeDefaultDescription
cols1 | 2 | 3 | 42Number 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>
PropTypeDefaultDescription
labelReact.ReactNode—Required. Small uppercase section label (e.g. "Products")
countnumber—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)
dividerbooleantrueRender 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.

PropTypeDefaultDescription
hrefstring—Required. Destination for the card + CTA
titleReact.ReactNode—Required. Primary label
descriptionReact.ReactNode—Supporting text
imageSrcstring—Hero image URL
imageAltstring""Alt text
ctaLabelReact.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 inside NavigationMenuContent with padding="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>}
/>
PropTypeDefaultDescription
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
showClosebooleantrue(bar only) Whether to render the built-in NavigationMenuClose button
closeLabelstring"Close menu"(bar only) ARIA label for the close button
titleReact.ReactNode—(promo only) Card headline
descriptionReact.ReactNode—(promo only) Supporting copy below the title
actionReact.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.

PropTypeDefaultDescription
hrefstring—Required. Link destination
titleReact.ReactNode—Required. Primary label
descriptionReact.ReactNode—Secondary descriptive text
iconReact.ReactNode—Optional leading icon
iconBackgroundbooleantrueRender the icon in a filled bg-secondary tile
tagReact.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.)
activebooleanfalseMarks 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>
SizeTrigger heightTypography
xsh-8text-sm
smh-10text-sm
mdh-12text-sm
lgh-14text-base
xlh-24text-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:

  • pill is 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).
  • underline is lighter and more editorial — inactive triggers sit in text-muted-foreground and 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

KeyAction
TabFocus the first trigger
ArrowRight / ArrowLeftMove between triggers (flips in RTL)
ArrowDown / Enter / SpaceOpen the focused trigger's popup and focus the first link
EscapeClose the popup and return focus to the trigger
Tab inside popupMove 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 on transition-* 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.

PropTypeDefaultDescription
openLabelstring"Open menu"ARIA label when the drawer is closed
closeLabelstring"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.

PropTypeDefaultDescription
rootHeaderReact.ReactNode—Header shown when on the L1 list
submenuHeader(activeItem: string) => React.ReactNode—Header shown when a submenu is active
footerReact.ReactNode—Rendered below the L1 list — typically a NavigationMenuMobileFooter
panelsReact.ReactNode—Submenu panels — typically NavigationMenuMobilePanel wrappers
titlestring"Navigation"<Dialog> ARIA title (visually hidden)
descriptionstring—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.

PropTypeDefaultDescription
labelReact.ReactNode—Section label rendered next to the arrow
ariaLabelstring"Back"ARIA label (used when no visible label is provided)

NavigationMenuMobileSection

Optional eyebrow-heading + divider wrapper for submenu content.

PropTypeDefaultDescription
labelReact.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).

PropTypeDefaultDescription
valuestring—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.

PropTypeDefaultDescription
languageReact.ReactNode—Language row (typically a NavigationMenuLanguageSelector) rendered below children

Mobile keyboard & a11y

KeyAction
TabFocus the NavigationMenuMobileTrigger
Enter / Space on triggerOpen the drawer
Tab inside drawerCycle through rows and interactive elements
Enter on an L1 rowDrill into the matching submenu panel
Enter on NavigationMenuBackTriggerPop back to the L1 list
EscapeClose 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-hidden when a submenu is active, and vice-versa, so screen-reader navigation stays in the visible stage.
  • Theme-aware logo swap observes <html> for a .dark class or data-theme="dark" — no SSR flicker because the first paint is deterministic (lightSrc before hydration).

Out of Scope (v1)

  • Full-page takeover navs (à la AE.1) — planned as a future NavOverlay component
  • 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 — mode is explicit. A future mode="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.

On this page

PlaygroundInstallationUsageMinimal menuFull mega menu (Deel-style)Mobile variantShowcaseAPI ReferenceNavigationMenuNavigationMenuBarNavigationMenuLogoNavigationMenuActionsNavigationMenuActionNavigationMenuLanguageSelectorNavigationMenuLanguageItemNavigationMenuListNavigationMenuItemNavigationMenuTriggerNavigationMenuItemLinkNavigationMenuContentNavigationMenuSectionHeadingNavigationMenuColumnsNavigationMenuContentHeaderNavigationMenuFeaturedNavigationMenuPopupFooterNavigationMenuCloseNavigationMenuLink (grid-cell helper)Size VariantsPopup RoundingPill vs. Underline TriggersSwap-style vs. Linear-style LinksRTLKeyboardAccessibilityMobile partsNavigationMenuMobileTriggerNavigationMenuMobileDrawerNavigationMenuMobileHeaderNavigationMenuBackTriggerNavigationMenuMobileSectionNavigationMenuMobilePanelNavigationMenuMobileFooterMobile keyboard & a11yOut of Scope (v1)