Menubar
Primitive

Menubar

Headless horizontal or vertical menubar primitives for application-style menu systems.

Live behavior

Menubar in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Interact with the specimen and inspect the behavior Atom contributes.

waiting for input

When to Use

Use Menubar for desktop-like application commands grouped under headings such as File, Edit, and View. Use Menu for one standalone command menu, NavigationMenu for website navigation, and NavList for a normal list of links. Menubar is usually unnecessary for a small set of buttons.

Features

The behavior Atom owns before your product adds appearance.

  • Renders a role="menubar" root with roving top-level trigger focus.
  • Supports horizontal and vertical orientation.
  • Opens adjacent top-level menus with ArrowLeft and ArrowRight.
  • Mirrors horizontal top-level navigation in RTL through dir or Direction.Provider.
  • Provides menu items, checkbox items, radio items, groups, separators, and submenus through the Menubar namespace.
  • Supports controlled and uncontrolled active top-level menu state.
  • Preserves Root and Content behavior through asChild and render composition.
  • Supports per-menu closeOnSelect, looping, and Escape close behavior.
  • Exposes state and styling data attributes without shipping styles.

Import

tsx
import { Menubar } from "@flowstack-ui/atom";

Anatomy

tsx
<Menubar.Root>
  <Menubar.Menu value="file">
    <Menubar.Trigger />
    <Menubar.Content>
      <Menubar.Arrow />
      <Menubar.Group>
        <Menubar.Label />
        <Menubar.Item />
        <Menubar.CheckboxItem />
        <Menubar.RadioGroup>
          <Menubar.RadioItem />
        </Menubar.RadioGroup>
      </Menubar.Group>
      <Menubar.Separator />
      <Menubar.Sub>
        <Menubar.SubTrigger />
        <Menubar.SubContent>
          <Menubar.Item />
        </Menubar.SubContent>
      </Menubar.Sub>
    </Menubar.Content>
  </Menubar.Menu>
</Menubar.Root>

API Reference

Menubar top-level coordination is implemented by Menubar.Root, Menubar.Menu, Menubar.Trigger, and Menubar.Content. The item, group, separator, and submenu parts share the same behavior as Menu, but are exposed directly on the Menubar namespace so consumers can build a complete menubar without importing Menu.

All DOM-rendering parts also accept their underlying native DOM props except for props Atom owns, such as managed roles and children.

Root

Contains the top-level menus and manages the active top-level menu value. Renders a div with role="menubar" and the resolved dir attribute.

PropTypeDefault
childrenReactNoderequired
asChildbooleanfalse
renderRenderProp-
valuestring | null-
defaultValuestring-
onValueChange(value: string | null) => void-
loopbooleantrue
dir"ltr" | "rtl"Direction.Provider
orientation"horizontal" | "vertical""horizontal"
classNamestring-
ARIA attributeValues
aria-orientationRoot orientation
Data attributeValues
[data-slot]"menubar" by default
[data-orientation]"horizontal" | "vertical"

Provides one top-level menu scope. Each Menu value must be unique within the same Root.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
closeOnSelectbooleantrue
loopbooleantrue
closeOnEscapebooleantrue

closeOnSelect is the default selection behavior for normal menu items inside that top-level menu. Checkbox and radio items default to staying open unless their own closeOnSelect prop is set.

Trigger

Opens and closes one top-level menu. Trigger renders a native button with role="menuitem" so it is a valid child of the menubar root. It uses roving tabIndex so only the active top-level trigger is tabbable.

PropTypeDefault
childrenReactNoderequired
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-
classNamestring-
ARIA attributeValues
aria-haspopup"menu"
aria-expandedtrue | false
aria-controlsAssociated menu content id
Data attributeValues
[data-slot]"menubar-trigger" by default
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Content

Renders the positioned top-level menu surface for a Menu as a portalled div with role="menu" and tabIndex={-1}.

PropTypeDefault
childrenReactNoderequired
asChildbooleanfalse
renderRenderProp-
side"top" | "right" | "bottom" | "left""bottom"
align"start" | "center" | "end""start"
sideOffsetnumber4
loopbooleanmenu value
ariaLabelstring-
classNamestring-
ARIA attributeValues
aria-orientation"vertical"
aria-labelFrom ariaLabel
aria-labelledbyTrigger id when ariaLabel is not provided
Data attributeValues
[data-slot]"menu-content" by default
[data-state]"open" | "closed"
[data-side]Resolved side
[data-align]Resolved align
[data-positioned]Present after positioning is ready

Item

Renders an actionable menu item with role="menuitem" and tabIndex={-1}.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
textValuestringchildren text or value
onSelect() => void-
disabledbooleanfalse
closeOnSelectbooleanmenu value
asChildbooleanfalse
renderRenderProp-
classNamestring-
ARIA attributeValues
aria-disabledPresent when disabled
Data attributeValues
[data-slot]"menu-item" by default
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Item value

CheckboxItem

Renders a menuitemcheckbox with tabIndex={-1}.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
textValuestringchildren text or value
checkedboolean | "indeterminate"false
onCheckedChange(checked: boolean) => void-
disabledbooleanfalse
closeOnSelectbooleanfalse
classNamestring-
ARIA attributeValues
aria-checkedtrue | false
aria-disabledPresent when disabled
Data attributeValues
[data-slot]"menu-checkbox-item" by default
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when checked
[data-value]Item value

RadioGroup

Provides radio selection state for RadioItem. Renders a div with role="group".

PropTypeDefault
childrenReactNoderequired
valuestring-
onValueChange(value: string) => void-
classNamestring-
Data attributeValues
[data-slot]"menu-radio-group" by default

Radio item values are scoped to their parent radio group for highlighting and keyboard movement, so separate groups can reuse values such as "default" inside the same menubar menu.

RadioItem

Renders a menuitemradio with tabIndex={-1}.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
textValuestringchildren text or value
disabledbooleanfalse
closeOnSelectbooleanfalse
classNamestring-
ARIA attributeValues
aria-checkedtrue | false
aria-disabledPresent when disabled
Data attributeValues
[data-slot]"menu-radio-item" by default
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when checked
[data-value]Public radio value

Group

Groups related menu items. Renders a div with role="group".

PropTypeDefault
childrenReactNoderequired
classNamestring-
Data attributeValues
[data-slot]"menu-group" by default

Separator

Renders a decorative separator with role="separator".

PropTypeDefault
classNamestring-
ARIA attributeValues
aria-orientation"horizontal"
Data attributeValues
[data-slot]"menu-separator" by default

Sub

Provides nested submenu state.

PropTypeDefault
childrenReactNoderequired
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-

SubTrigger

Renders the item that opens a nested submenu with role="menuitem" and tabIndex={-1}. Mouse hover opening starts only after the pointer actually moves over the item, so newly positioned content beneath a stationary pointer does not open it.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
textValuestringchildren text or value
disabledbooleanfalse
classNamestring-
ARIA attributeValues
aria-haspopup"menu"
aria-expandedtrue | false
aria-disabledPresent when disabled
Data attributeValues
[data-slot]"menu-sub-trigger" by default
[data-state]"open" | "closed"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Trigger value

SubContent

Renders the positioned nested submenu surface as a portalled div with role="menu" and tabIndex={-1}.

PropTypeDefault
childrenReactNoderequired
sideOffsetnumber4
loopbooleantrue
ariaLabelstring-
classNamestring-
ARIA attributeValues
aria-orientation"vertical"
aria-labelFrom ariaLabel
aria-labelledbySubTrigger id when ariaLabel is not provided
Data attributeValues
[data-menu-sub-content]Present
[data-slot]"menu-sub-content" by default
[data-state]"open" | "closed"
[data-side]Resolved side
[data-positioned]Present after positioning is ready

Advanced top-level parts can use useMenubarContext and useMenubarMenuContext; the matching providers and context value types are also public exports. Shared item parts use the Menu contexts documented above.

Examples

Application Menu

tsx
import { Menubar } from "@flowstack-ui/atom";

export function ApplicationMenubar() {
  return (
    <Menubar.Root aria-label="Application commands">
      <Menubar.Menu value="file">
        <Menubar.Trigger>File</Menubar.Trigger>
        <Menubar.Content ariaLabel="File">
          <Menubar.Item value="new" onSelect={() => console.log("New file")}>
            New
          </Menubar.Item>
          <Menubar.Sub>
            <Menubar.SubTrigger value="export">Export</Menubar.SubTrigger>
            <Menubar.SubContent ariaLabel="Export">
              <Menubar.Item value="pdf">PDF</Menubar.Item>
              <Menubar.Item value="csv">CSV</Menubar.Item>
            </Menubar.SubContent>
          </Menubar.Sub>
        </Menubar.Content>
      </Menubar.Menu>
      <Menubar.Menu value="edit">
        <Menubar.Trigger>Edit</Menubar.Trigger>
        <Menubar.Content ariaLabel="Edit">
          <Menubar.Item value="undo">Undo</Menubar.Item>
        </Menubar.Content>
      </Menubar.Menu>
    </Menubar.Root>
  );
}

Selection Menu

tsx
import { useState } from "react";
import { Menubar } from "@flowstack-ui/atom";

export function ViewMenubar() {
  const [statusBar, setStatusBar] = useState(true);
  const [density, setDensity] = useState("comfortable");

  return (
    <Menubar.Root aria-label="View commands">
      <Menubar.Menu value="view">
        <Menubar.Trigger>View</Menubar.Trigger>
        <Menubar.Content ariaLabel="View">
          <Menubar.CheckboxItem
            value="status-bar"
            checked={statusBar}
            onCheckedChange={setStatusBar}
          >
            Status bar
          </Menubar.CheckboxItem>
          <Menubar.RadioGroup value={density} onValueChange={setDensity}>
            <Menubar.RadioItem value="compact">Compact</Menubar.RadioItem>
            <Menubar.RadioItem value="comfortable">Comfortable</Menubar.RadioItem>
          </Menubar.RadioGroup>
        </Menubar.Content>
      </Menubar.Menu>
    </Menubar.Root>
  );
}

Portal, Arrow, Label, and ItemIndicator use the shared Menu contract. CheckboxItem supports mixed state and all retained DOM parts accept refs, native props, asChild, and render. Hover switching is mouse-only; touch and pen use click/tap.

Accessibility

Follows the WAI-ARIA menubar pattern. Top-level triggers use roving focus and open menus with keyboard or pointer input. Top-level triggers expose role="menuitem" so the role="menubar" root has valid menuitem children.

Horizontal ArrowLeft and ArrowRight navigation and nested submenu placement mirror in RTL when dir="rtl" is set on Menubar.Root or inherited from Direction.Provider. When an open menubar moves between top-level triggers, focus remains owned by the active trigger so Enter, Space, and Escape target the active menu.

Pointer/tap-opened menus focus the first item; ArrowDown and ArrowUp seed the first and last item for keyboard opening. Portalled menu content registers with a parent modal focus scope when opened inside Dialog, Drawer, or another modal primitive. Menubar menu content inherits Menu typeahead behavior for printable-character searches.

KeyDescription
ArrowRight / ArrowLeftMoves horizontal top-level focus, mirrored in RTL
ArrowDown / ArrowUpMoves vertical top-level focus; in horizontal mode opens first/last item
Home / EndMoves to the first or last top-level trigger, or item inside open content
Enter / Space on triggerOpens or closes the top-level menu
Enter / Space on itemSelects the focused item unless disabled
EscapeCloses the topmost submenu first, then the active top-level menu
Printable characterTypeahead search inside open menu content
Tab / Shift+TabCloses and exits after/before the complete Menubar Root

Changelog

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.20.6

  • Inherited movement-gated submenu hover intent so opening a top-level menu cannot also open a submenu that appears beneath a stationary pointer.

0.12.1

  • Fixed Root and Content to preserve their behavior, refs, native props, and children through the documented asChild and render composition paths.
  • Fixed Content composition to preserve consumer onKeyDownCapture handlers before Menubar-owned adjacent-menu navigation.

0.12.0

  • Added horizontal/vertical orientation with matching ARIA/data state and orientation-aware top-level roving focus.
  • Added Trigger ref, asChild, and render composition and made hover switching mouse-only while keeping click/tap universal.
  • Inherited real menu-item focus, complete shared anatomy, mixed state, geometry variables, submenu corrections, and whole-Menubar Tab exit.

0.3.1

  • Inherited reliable Menu exit-presence cleanup for closed Menubar and submenu Content under global motion CSS.

0.2.0

  • Fixed local Menubar.Root dir="rtl" so shared nested submenu placement also mirrors to the left, matching Direction.Provider dir="rtl".
  • Fixed adjacent top-level menu handoff so the active trigger keeps focus for Enter, Space, and Escape after ArrowLeft or ArrowRight navigation.
  • Fixed custom data-slot overrides on Menubar.Root and Menubar.Trigger.
  • Added Direction.Provider and dir support to mirror Menubar top-level ArrowLeft and ArrowRight navigation in RTL.
  • Inherited the shared Menu typeahead behavior so a single-character search cycles from the current matching item while multi-character buffers still match exact prefixes.
  • Fixed pointer-open behavior so clicking or hovering between top-level menus opens content without pre-highlighting the first item; keyboard ArrowDown and ArrowUp still seed first and last item highlight.
  • Registered shared Menu content with parent modal focus scopes so Menubar menus can remain valid focus targets inside Dialog, Drawer, and other modal primitives.
  • Fixed Menubar.Trigger semantics so top-level triggers expose role="menuitem" as valid children of the role="menubar" root.
  • Inherited the shared Menu radio item fix so separate Menubar radio groups can reuse the same public values without sharing highlight state.
  • Inherited the shared Menu highlight fix so pointer movement over non-item content or item gaps does not reset highlight back to the first item.
  • Added shared menu item parts to the Menubar namespace object.
  • Refined Menubar.Content keyboard handler dependencies to avoid recreating callbacks from the full context objects.

0.1.0

  • Initial Atom release.