Navigation

Menubar

A desktop-style menubar: once a menu is open, hovering a neighbour switches instantly and the panel slides to its new position instead of closing and reopening, with full arrow-key navigation.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

The "File / Edit / View" bar across the top of a desktop app. Once any menu is open, moving the pointer onto a neighbouring menu name switches to it straight away, no extra click. The switch does not close the old panel and open a new one: the same panel slides to its new position on a spring, resizes as it goes, and crossfades its content. The item layer matches the dropdown menu — plain items, checkbox and radio items, submenus, group labels, separators and shortcut hints — and on the keyboard ←/→ move between menus while ↑/↓ move between items.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/menubar.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

100
500
150
Active edited usageDefault props
<Menubar/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/menubar.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/menubar.

Usage

import { Menubar } from "@/components/ui/menubar";

const [lineNumbers, setLineNumbers] = React.useState(true);
const [fontSize, setFontSize] = React.useState("md");

<Menubar>
  <Menubar.Menu value="file">
    <Menubar.Trigger>檔案</Menubar.Trigger>
    <Menubar.Content>
      <Menubar.Item onSelect={() => console.log("新增")}>
        新增
        <Menubar.Shortcut>⌘N</Menubar.Shortcut>
      </Menubar.Item>
      <Menubar.Sub>
        <Menubar.SubTrigger>最近開啟</Menubar.SubTrigger>
        <Menubar.SubContent>
          <Menubar.Item>週會記錄.md</Menubar.Item>
          <Menubar.Item>旅行清單.md</Menubar.Item>
        </Menubar.SubContent>
      </Menubar.Sub>
      <Menubar.Separator />
      <Menubar.Item>匯出為 PDF</Menubar.Item>
    </Menubar.Content>
  </Menubar.Menu>
  <Menubar.Menu value="view">
    <Menubar.Trigger>檢視</Menubar.Trigger>
    <Menubar.Content>
      <Menubar.CheckboxItem checked={lineNumbers} onCheckedChange={setLineNumbers}>
        顯示行號
      </Menubar.CheckboxItem>
      <Menubar.Separator />
      <Menubar.Label inset>字級</Menubar.Label>
      <Menubar.RadioGroup value={fontSize} onValueChange={setFontSize}>
        <Menubar.RadioItem value="sm">小</Menubar.RadioItem>
        <Menubar.RadioItem value="md">中</Menubar.RadioItem>
        <Menubar.RadioItem value="lg">大</Menubar.RadioItem>
      </Menubar.RadioGroup>
    </Menubar.Content>
  </Menubar.Menu>
</Menubar>

Props

PropTypeDefaultDescription
valuestring—Controlled: the value of the open Menu; '' means all closed
defaultValuestring''Initially open Menu in uncontrolled mode
onValueChange(value: string) => void—Called when the open Menu changes ('' when everything closes)
loopbooleantrueWhether arrow keys wrap at the ends (between Triggers and between menu items)
durationnumber100Duration of open/close and the content crossfade (ms)
switchStiffnessnumber500Spring stiffness of the panel slide when switching menus
switchDampingnumber40Spring damping of the panel slide when switching menus
subDelaynumber150How long the pointer must rest on a submenu item before it expands (ms)
classNamestring—Passed through to the menubar root element
FieldTypeDefaultDescription
valuestring—Unique value for this menu (required), used by value/onValueChange
FieldTypeDefaultDescription
disabledbooleanfalseDisabled: cannot open, skipped by left/right navigation
classNamestring—Passed through to the trigger button; other button attributes pass through too
FieldTypeDefaultDescription
classNamestring—Passed through to the menu panel (e.g. min-w-56 to change the minimum width)
FieldTypeDefaultDescription
onSelect() => void—Called when selected (click, Enter or Space)
disabledbooleanfalseDisabled: cannot be selected, skipped by keyboard navigation
insetbooleanfalseReserve icon space on the left to align with checkable items
textValuestring—Text used for typeahead; defaults to the item's text content
closeOnSelectbooleantrueWhether selecting closes the menu
classNamestring—Passed through to the item
FieldTypeDefaultDescription
checkedboolean—Controlled checked state
defaultCheckedbooleanfalseInitial checked state in uncontrolled mode
onCheckedChange(checked: boolean) => void—Called when the checked state changes
onSelect() => void—Called when selected
disabledbooleanfalseDisabled
textValuestring—Text used for typeahead
closeOnSelectbooleanfalseWhether selecting closes the menu (checkbox items stay open by default)
classNamestring—Passed through to the item
FieldTypeDefaultDescription
valuestring—Controlled selected value
defaultValuestring—Initial selected value in uncontrolled mode
onValueChange(value: string) => void—Called when the selected value changes
classNamestring—Passed through to the group container
FieldTypeDefaultDescription
valuestring—The value this option represents (required)
onSelect() => void—Called when selected
disabledbooleanfalseDisabled
textValuestring—Text used for typeahead
closeOnSelectbooleanfalseWhether selecting closes the menu (radio items stay open by default)
classNamestring—Passed through to the item
FieldTypeDefaultDescription
openboolean—Controlled submenu open state
defaultOpenbooleanfalseInitial open state in uncontrolled mode
onOpenChange(open: boolean) => void—Called when the submenu open state changes
FieldTypeDefaultDescription
disabledbooleanfalseDisabled
insetbooleanfalseReserve icon space on the left to align with checkable items
textValuestring—Text used for typeahead
classNamestring—Passed through to the item
FieldTypeDefaultDescription
sideOffsetnumber-4Distance from the SubTrigger (px); negative values overlap the parent menu slightly
alignOffsetnumber-5Cross-axis offset (px); the default lines the first item up with the SubTrigger
collisionPaddingnumber8Minimum distance kept from the viewport edges (px)
classNamestring—Passed through to the submenu panel

Label / Group / Separator / Shortcut

FieldTypeDefaultDescription
Label.insetbooleanfalseGroup heading (non-interactive); with inset it lines up with checkable item text
Group——Wraps a set of items in role="group"
Separator——A thin role="separator" divider
Shortcut——Right-aligned muted monospace hint; a label only, binding the key is up to you

Every part accepts className, merged with cn().

How it works

  • Switching: with no menu open, a Trigger opens on click; once one is open, moving the pointer onto another Trigger switches instantly with no delay. Clicking the currently open Trigger again closes it.
  • One panel container: switching never plays "old panel out + new panel in". The same container tweens its position and size from the old anchor to below the new Trigger on a spring (switchStiffness/switchDamping) while the content crossfades over duration. Only going from fully closed to open, or open to fully closed, plays the fade + scale-from-95% + 8px slide entrance and exit.
  • Positioning: the panel is position: fixed and portaled to body, pinned below the Trigger, start-aligned, 4px away; if it does not fit below and there is more room above it flips upward, and it is pushed back inside the viewport with 8px to spare horizontally. Viewport resizes, scrolling of any ancestor, and Trigger or panel size changes are recomputed with requestAnimationFrame throttling — those recomputes jump straight there instead of springing, so the panel never lags behind its anchor.
  • The duration is also written to --wb-menubar-duration; every CSS transition inside the panel reads that variable, so you can override it globally.
  • Submenus: expand after resting subDelay ms, anchored to the SubTrigger on the right (flipping left if there is no room); when the pointer heads diagonally to a submenu, a safe triangle made from the exit point and the submenu's two near corners keeps the submenu from closing as the pointer brushes past other items.
  • Typeahead: characters typed within 500ms accumulate into a prefix matched against textValue (defaults to the item text, ignoring shortcuts); CJK text matches on the first character.
  • Clicking outside the menubar and all panels closes the menu and leaves focus with whatever was clicked.

Accessibility

  • The root is role="menubar" with aria-orientation="horizontal"; each Trigger is role="menuitem" with aria-haspopup="menu", aria-expanded and aria-controls
  • Triggers use a roving tabindex: the whole menubar is a single Tab stop, ←/→ move between Triggers (loop controls wrapping), Home/End jump to the ends
  • With a menu open, ←/→ (focus inside the panel and not on a SubTrigger) switch to the adjacent menu and move focus to its first item
  • On a Trigger, ↓/Enter/Space open the menu and focus the first item, ↑ opens it and focuses the last; inside the panel ↑↓ cycle through enabled items, Home/End jump to the ends, Enter/Space select, Escape closes and returns to the Trigger, and Tab closes and lets focus move on naturally
  • The panel is role="menu" with aria-orientation="vertical"; items use menuitem, menuitemcheckbox or menuitemradio by kind, and checkbox and radio items carry aria-checked; disabled items carry aria-disabled and are skipped; shortcut hints are hidden from assistive technology
  • Submenus are entered with → or Enter and left with ← or Escape, which returns to the SubTrigger
  • Typeahead and Enter do nothing while an IME (e.g. Chinese) is composing (isComposing or keyCode 229)
  • When the system "reduce motion" setting is on, the panel keeps only its fade (same duration) with no scaling or sliding; switching menus jumps the panel straight to its new position, and the highlight and radio dot snap into place too