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.
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>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled: the value of the open Menu; '' means all closed |
defaultValue | string | '' | Initially open Menu in uncontrolled mode |
onValueChange | (value: string) => void | — | Called when the open Menu changes ('' when everything closes) |
loop | boolean | true | Whether arrow keys wrap at the ends (between Triggers and between menu items) |
duration | number | 100 | Duration of open/close and the content crossfade (ms) |
switchStiffness | number | 500 | Spring stiffness of the panel slide when switching menus |
switchDamping | number | 40 | Spring damping of the panel slide when switching menus |
subDelay | number | 150 | How long the pointer must rest on a submenu item before it expands (ms) |
className | string | — | Passed through to the menubar root element |
Menubar.Menu
| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique value for this menu (required), used by value/onValueChange |
Menubar.Trigger
| Field | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disabled: cannot open, skipped by left/right navigation |
className | string | — | Passed through to the trigger button; other button attributes pass through too |
Menubar.Content
| Field | Type | Default | Description |
|---|---|---|---|
className | string | — | Passed through to the menu panel (e.g. min-w-56 to change the minimum width) |
Menubar.Item
| Field | Type | Default | Description |
|---|---|---|---|
onSelect | () => void | — | Called when selected (click, Enter or Space) |
disabled | boolean | false | Disabled: cannot be selected, skipped by keyboard navigation |
inset | boolean | false | Reserve icon space on the left to align with checkable items |
textValue | string | — | Text used for typeahead; defaults to the item's text content |
closeOnSelect | boolean | true | Whether selecting closes the menu |
className | string | — | Passed through to the item |
Menubar.CheckboxItem
| Field | Type | Default | Description |
|---|---|---|---|
checked | boolean | — | Controlled checked state |
defaultChecked | boolean | false | Initial checked state in uncontrolled mode |
onCheckedChange | (checked: boolean) => void | — | Called when the checked state changes |
onSelect | () => void | — | Called when selected |
disabled | boolean | false | Disabled |
textValue | string | — | Text used for typeahead |
closeOnSelect | boolean | false | Whether selecting closes the menu (checkbox items stay open by default) |
className | string | — | Passed through to the item |
Menubar.RadioGroup
| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled selected value |
defaultValue | string | — | Initial selected value in uncontrolled mode |
onValueChange | (value: string) => void | — | Called when the selected value changes |
className | string | — | Passed through to the group container |
Menubar.RadioItem
| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | The value this option represents (required) |
onSelect | () => void | — | Called when selected |
disabled | boolean | false | Disabled |
textValue | string | — | Text used for typeahead |
closeOnSelect | boolean | false | Whether selecting closes the menu (radio items stay open by default) |
className | string | — | Passed through to the item |
Menubar.Sub
| Field | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled submenu open state |
defaultOpen | boolean | false | Initial open state in uncontrolled mode |
onOpenChange | (open: boolean) => void | — | Called when the submenu open state changes |
Menubar.SubTrigger
| Field | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disabled |
inset | boolean | false | Reserve icon space on the left to align with checkable items |
textValue | string | — | Text used for typeahead |
className | string | — | Passed through to the item |
Menubar.SubContent
| Field | Type | Default | Description |
|---|---|---|---|
sideOffset | number | -4 | Distance from the SubTrigger (px); negative values overlap the parent menu slightly |
alignOffset | number | -5 | Cross-axis offset (px); the default lines the first item up with the SubTrigger |
collisionPadding | number | 8 | Minimum distance kept from the viewport edges (px) |
className | string | — | Passed through to the submenu panel |
Label / Group / Separator / Shortcut
| Field | Type | Default | Description |
|---|---|---|---|
Label.inset | boolean | false | Group 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 overduration. 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: fixedand portaled tobody, 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
subDelayms, 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"witharia-orientation="horizontal"; each Trigger isrole="menuitem"witharia-haspopup="menu",aria-expandedandaria-controls - Triggers use a roving tabindex: the whole menubar is a single Tab stop, ←/→ move between Triggers (
loopcontrols 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"witharia-orientation="vertical"; items usemenuitem,menuitemcheckboxormenuitemradioby kind, and checkbox and radio items carryaria-checked; disabled items carryaria-disabledand 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 (
isComposingor 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
Pagination
A React pagination component whose current-page pill slides between numbers, with expandable ellipses that stagger in hidden pages, a compact “3 / 12” variant with rolling digits, arrow-key navigation and an SEO-friendly link mode.
Glass Tab Bar
A frosted-glass mobile bottom tab bar React component. Icons pop in one by one on mount, the active pill slides between tabs and the active label expands, with an optional raised centre button and unread badges.