Menubar
A menubar groups several menus — like the File / Edit / View bar in a desktop app — into a single widget. Each menu is a regular Zag.js menu; the menubar coordinates focus and open state between them.
Features
- Acts as a single tab stop, with arrow keys moving focus between menus.
- Once a menu is open, hovering or arrowing to a sibling switches to it without a click.
- Typeahead to jump to a menu by typing its label.
- Works with nested submenus.
- Horizontal or vertical orientation.
Installation
Install the menubar package. You'll use it alongside @zag-js/menu.
Usage
A menubar is made of two machines: one menubar machine for the bar, and one
menu machine for each top-level menu.
import * as menu from "@zag-js/menu" import * as menubar from "@zag-js/menubar"
The menubar gives each menu a small config object through api.getMenuContext().
Pass it to the menu's menubar prop — that's what makes the menu behave as part
of the bar instead of a standalone dropdown. A context is the easiest way to
thread it down:
const MenubarContext = createContext<menu.MenubarContext | undefined>(undefined) function Menu(props: { id: string; label: string; items: string[] }) { const menubarContext = useContext(MenubarContext) const service = useMachine(menu.machine, { id: props.id, menubar: menubarContext }) const api = menu.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps()}>{props.label}</button> {api.open && ( <Portal> <div {...api.getPositionerProps()}> <ul {...api.getContentProps()}> {props.items.map((item) => ( <li key={item} {...api.getItemProps({ value: item })}> {item} </li> ))} </ul> </div> </Portal> )} </> ) } function Menubar() { const service = useMachine(menubar.machine, { id: useId() }) const api = menubar.connect(service, normalizeProps) return ( <MenubarContext.Provider value={api.getMenuContext()}> <div {...api.getRootProps()}> <Menu id="file" label="File" items={["New", "Open", "Save"]} /> <Menu id="edit" label="Edit" items={["Undo", "Redo"]} /> <Menu id="view" label="View" items={["Zoom In", "Zoom Out"]} /> </div> </MenubarContext.Provider> ) }
The config carries the menubar's rootId, disabled, and orientation, so a
menu always reflects the bar it belongs to.
Nested submenus
A submenu inside a menubar menu works like any nested menu — link it to its
parent with setChild / setParent and render its trigger with
getTriggerItemProps.
const parent = useMachine(menu.machine, { id: "edit", menubar: menubarContext }) const sub = useMachine(menu.machine, { id: "find" }) useEffect(() => { parentApi.setChild(sub) subApi.setParent(parent) }, [])
Only the top-level menu gets the menubar prop. The submenu doesn't — it's a
child of the menu, not of the bar.
With the keyboard, ArrowRight on a submenu trigger opens the submenu, while
ArrowRight on a regular item moves to the next menu in the bar.
Vertical orientation
Set orientation to "vertical" to stack the triggers. Use positioning so the
menus open to the side instead of below.
const service = useMachine(menubar.machine, { id: useId(), orientation: "vertical", }) // each menu const service = useMachine(menu.machine, { id, menubar: menubarContext, positioning: { placement: "right-start" }, })
In a vertical menubar, Up / Down move between triggers, ArrowRight opens a
menu, and ArrowLeft closes it back to its trigger.
Disabling the menubar
Set disabled on the menubar to disable every trigger at once.
const service = useMachine(menubar.machine, { id: useId(), disabled: true, })
Looping focus
By default, arrowing past the first or last trigger wraps around. Set
loopFocus to false to stop at the ends.
const service = useMachine(menubar.machine, { id: useId(), loopFocus: false, })
Styling guide
The menubar root exposes a few attributes you can target in CSS.
[data-part="root"][data-orientation="horizontal|vertical"] { /* styles per orientation */ } [data-part="root"][data-disabled] { /* styles when the whole bar is disabled */ } /* present while any menu in the bar is open */ [data-part="root"][data-has-open-menu="true"] { /* ... */ }
Triggers are owned by the menu, so style them with the menu's trigger part. See
the menu styling guide.
Methods and Properties
Machine Context
The menubar machine exposes the following context properties:
idsElementIds | undefinedThe ids of the elements in the menubar. Useful for composition.orientationOrientation | undefinedThe orientation of the menubar.loopFocusboolean | undefinedWhether to loop the keyboard navigation across the triggers.disabledboolean | undefinedWhether the menubar (and all its menu triggers) is disabled.dir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Document | Node) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The menubar api exposes the following methods:
hasOpenMenubooleanWhether any menu within the menubar is open.orientationOrientationThe orientation of the menubar.disabledbooleanWhether the menubar is disabled.getMenuContext() => MenubarMenuContextReturns the config to pass to each top-level menu's `menubar` prop. Adapters usually expose this via a context so nested `<Menu>`s can read it.
Data Attributes
Accessibility
Follows the WAI-ARIA Menubar pattern — the bar is a single tab stop and arrow keys move focus between menus.
Keyboard Interactions
- ArrowRightArrowLeftIn a horizontal menubar, moves focus to the next/previous menu. If a menu is open, opens the sibling menu.
- ArrowDownArrowUpIn a vertical menubar, moves focus to the next/previous menu. In a horizontal menubar, opens the focused menu.
- EnterSpaceOpens the focused menu and highlights its first item.
- HomeEndMoves focus to the first/last menu.
- EscCloses the open menu and returns focus to its trigger.