Components
Menubar
A desktop-style application menu bar — a persistent horizontal set of dropdown menus for organizing commands, shortcuts, and toggles.
When to use it
Menubar is for editor-shaped screens — documents, spreadsheets, anything that needs a fixed, exhaustive grouping of commands within one workspace. It is not the primary navigation of an app: moving between areas of a Comitor product is the job of Sidebar and AppHeader from @comitor/ui/shell. If a menu here would take the user to a different page rather than act on the current one, it belongs in the shell instead.
Basic usage
Compose MenubarMenu blocks inside a Menubar. Open state is managed by Radix, so no useState is required.
Checkbox & radio items
Use MenubarCheckboxItem for toggles and MenubarRadioGroup for single-choice options.
Both are controlled only. The Radix menu primitives read checked (defaulting to false) and value; unlike the standalone Checkbox and RadioGroup they have no defaultChecked or defaultValue. Passing one type-checks — React allows those attributes on any element — but it lands on the underlying <div> and the row never ticks. The preview below holds its own state and passes onCheckedChange / onValueChange. (The defaultValue in the Menubar root table below is a different prop and does work — it belongs to the bar itself, not to a radio group.)
Submenus & destructive items
Nest MenubarSub for cascading menus, and pass variant="destructive" to flag dangerous actions.
The package renders MenubarSubContent inside its own portal. MenubarContent carries overflow-hidden to keep the panel clipped to its rounded corners, and its open animation leaves a transform behind, which makes the panel a containing block even for position: fixed children. Without the portal a submenu opens beside its parent and is clipped away — the chevron row reacts to nothing, and nothing is logged.
Compact density
Menu rows measure their vertical padding with py-(--menu-item-py) rather than a fixed py-1.5, so a long command list tightens from 6px to 4px per row under data-density="compact" — the same axis that tightens table rows and sidebar items. Only the vertical axis moves. In an app the attribute is set once on <html> by DensityProvider from @comitor/ui/shell; the panel below scopes it by hand so you can compare it with the menus above.
Props
The parts exported from @comitor/ui. Every part forwards the props of the Radix primitive it wraps, plus the native props of the element it renders.
| Prop | Type | Default | Description |
|---|---|---|---|
Menubar | MenubarPrimitive.Root | — | The horizontal bar itself — h-9, gap-1, rounded border on the page background. Manages roving focus between menus. |
MenubarMenu | MenubarPrimitive.Menu | — | Wraps one trigger and its content. Accepts an optional value to address it from the root. |
MenubarTrigger | MenubarPrimitive.Trigger | — | The button in the bar. Tints to the accent token while its menu is open. |
MenubarContent | MenubarPrimitive.Content | — | The floating panel of items, portalled to document.body. |
MenubarPortal | MenubarPrimitive.Portal | — | Escape hatch for portalling content yourself — Content and SubContent already portal, so you rarely need it. |
MenubarGroup | MenubarPrimitive.Group | — | Groups related items so assistive tech announces them together. |
MenubarItem | MenubarPrimitive.Item | — | A selectable command row. See the item props below. |
MenubarCheckboxItem | MenubarPrimitive.CheckboxItem | — | Toggle row with a check indicator. Controlled only — checked (default false) plus onCheckedChange; there is no defaultChecked. |
MenubarRadioGroup | MenubarPrimitive.RadioGroup | — | Groups radio items and tracks the selected value. Controlled only — value plus onValueChange; there is no defaultValue. |
MenubarRadioItem | MenubarPrimitive.RadioItem | — | Single-choice row with a dot indicator; requires a unique value. |
MenubarLabel | MenubarPrimitive.Label | — | Non-interactive section heading. Accepts inset to align with item text. |
MenubarSeparator | MenubarPrimitive.Separator | — | Full-bleed divider drawn with the decorative border token. |
MenubarShortcut | span | — | Muted, right-aligned shortcut text. Use KeyboardHint from the same entry when the combo contains a modifier. |
MenubarSub | MenubarPrimitive.Sub | — | Wraps a nested submenu (SubTrigger + SubContent). |
MenubarSubTrigger | MenubarPrimitive.SubTrigger | — | Row that opens a submenu on hover or arrow key; accepts inset and appends a chevron. |
MenubarSubContent | MenubarPrimitive.SubContent | — | The submenu panel, portalled so the parent panel cannot clip it. |
Menubar (root)
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled value of the open menu, matching a MenubarMenu value. |
defaultValue | string | '' | Which menu is open on first render, for uncontrolled usage. |
onValueChange | (value: string) => void | — | Called when a different menu opens or all of them close. |
loop | boolean | true | Whether arrow-key focus wraps from the last trigger back to the first. |
dir | 'ltr' | 'rtl' | 'ltr' | Reading direction, affecting arrow keys and submenu placement. |
MenubarContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | Alignment against the trigger. The package overrides the Radix default of center. |
alignOffset | number | -4 | Offset along the alignment axis — pulls the panel left so its padding lines up with the trigger label. |
sideOffset | number | 8 | Gap in pixels between the bar and the panel. |
loop | boolean | false | Whether keyboard navigation wraps from the last item back to the first. |
className | string | — | Additional Tailwind classes. The panel is min-w-[12rem] by default. |
MenubarItem
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'destructive' | 'default' | Styles the row — destructive paints the label, icon and focus tint in the destructive ink token. |
inset | boolean | false | Adds left padding to align with items that have a leading icon or indicator. |
disabled | boolean | false | Disables the item, removing pointer events and dimming it. |
onSelect | (event: Event) => void | — | Called when the item is selected via pointer or keyboard. |
textValue | string | — | Text used for typeahead matching when the item contains more than plain text. |
asChild | boolean | false | Merges the item props onto its single child — use it to render a link as a menu item. |
Accessibility
- Built on Radix Menubar, which implements the WAI-ARIA
menubarpattern with the correct roles and relationships. - Full keyboard support: ← / → move between menus, ↑ / ↓ move between items, and Esc closes the open menu.
- Type-ahead lets users jump to an item by typing its label, and focus is trapped within the open menu until it closes.
- Disabled items set
data-disabled, removing pointer events and skipping them in keyboard navigation. variant="destructive"paints the label in the destructive ink step, not the destructive fill. The fill is a button background pinned to one value across both themes and only reaches 3.53:1 as text on the popover surface — worse on focus, where the row tints tobg-destructive/20(2.98:1). The ink step clears both themes and both backgrounds: 7.15:1 / 6.18:1 in light at rest and on focus, 6.42:1 / 5.42:1 in dark.- Shortcut glyphs typed into
MenubarShortcutare shown verbatim, so a hard-coded⌘is wrong for every non-Mac user. UseKeyboardHintfrom@comitor/uiwithkeys="mod+b"to resolve the modifier per platform; it isaria-hidden, so name the shortcut in the item's accessible name when it matters.