implement
Menubar

Menubar

A row of menus that hand off to one another, the way an application menu bar does.

import { signal } from "@implementjs/core";
import {
	Menubar,
	MenubarCheckboxItem,
	MenubarContent,
	MenubarItem,
	MenubarMenu,
	MenubarRadioGroup,
	MenubarRadioItem,
	MenubarSeparator,
	MenubarTrigger,
} from "@/lib/components/ui/menubar";

export default function MenubarDemo() {
	const wordWrap = signal(true);
	const theme = signal<string | null>("system");

	return Menubar(
		MenubarMenu(
			{ value: "file" },
			MenubarTrigger("File"),
			MenubarContent(
				{ class: "w-48" },
				MenubarItem({ onSelect: () => console.log("new") }, "New file"),
				MenubarItem({ onSelect: () => console.log("open") }, "Open…"),
				MenubarSeparator(),
				MenubarItem({ onSelect: () => console.log("save") }, "Save"),
				MenubarItem({ disabled: true }, "Save all"),
			),
		),
		MenubarMenu(
			{ value: "edit" },
			MenubarTrigger("Edit"),
			MenubarContent(
				{ class: "w-48" },
				MenubarItem({ onSelect: () => console.log("undo") }, "Undo"),
				MenubarItem({ onSelect: () => console.log("redo") }, "Redo"),
				MenubarSeparator(),
				MenubarItem({ onSelect: () => console.log("find") }, "Find…"),
			),
		),
		MenubarMenu(
			{ value: "view" },
			MenubarTrigger("View"),
			MenubarContent(
				{ class: "w-48" },
				MenubarCheckboxItem({ checked: wordWrap, closeOnSelect: false }, "Word wrap"),
				MenubarSeparator(),
				MenubarRadioGroup(
					{ value: theme },
					MenubarRadioItem({ value: "light" }, "Light"),
					MenubarRadioItem({ value: "dark" }, "Dark"),
					MenubarRadioItem({ value: "system" }, "System"),
				),
			),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/menubar

jsrepo pulls dropdown-menu along with it.

Usage

A menubar is a row of dropdown menus that share focus: once one is open, moving along the bar opens the next without another click, and the arrow keys walk the whole bar.

The bar itself is styled as a bordered strip; the triggers are compact titles that fill in while highlighted or open. The panels and items are the shared menu styles.

import {
	Menubar,
	MenubarContent,
	MenubarItem,
	MenubarMenu,
	MenubarSeparator,
	MenubarTrigger,
} from "@/lib/components/ui/menubar";

Menubar(
	MenubarMenu(
		{ value: "file" },
		MenubarTrigger("File"),
		MenubarContent(
			{ class: "w-48" },
			MenubarItem({ onSelect: newFile }, "New file"),
			MenubarSeparator(),
			MenubarItem({ disabled: true }, "Save all"),
		),
	),
);

Every menu needs a value

MenubarMenu takes a value — a stable string the bar tracks as its open menu. Pass a signal as the root's value to open one from outside, or to know which is open.

Shared with the dropdown menu

The panels, items, separators, and indicators come from dropdown-menu.ts, which is why installing the menubar brings that file too.

API Reference

Every prop the styling does not consume is forwarded to the Menubar primitive, so the tables below are the whole surface — the behavior props and the styling ones together.

The bar. Sets role="menubar", owns which menu is open, and moves focus between the triggers. Styled as a bordered bar the menu triggers sit in. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<string | null> | string | nullnullThe open menu's value, or null while all are closed. Pass a signal to control it from outside.
loopbooleantrueWhether arrow keys wrap from the last trigger back to the first.
Data attributeValue
[data-menubar-root]Present
[data-orientation]"horizontal"

One menu of the bar. Wraps a trigger and its content.

PropTypeDefaultDescription
value*stringIdentifies the menu. Must be unique within the menubar.
preventScrollbooleantrueWhen true, the page behind cannot scroll while this menu is open. The panel can still scroll if you give it overflow.

The menu's button in the bar. Sets role="menuitem"; hovering it switches to this menu while another is open. Styled as a compact menu title that fills in while its menu is highlighted or open. Renders a Button; extra props are forwarded onto it.

PropTypeDefaultDescription
disabledSignal<boolean> | booleanfalsePrevents opening the menu. Sets disabled and data-disabled.
Data attributeValue
[data-menubar-trigger]Present
[data-state]"open" | "closed"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

The floating panel of items. Sets role="menu". Styled as a popover panel that fades and scales in from the side it opens on, and hides itself when closed. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""bottom"Preferred side of the anchor to place the panel.
align"start" | "center" | "end""start"How the panel aligns along the chosen side.
offsetnumber8Distance in pixels between the anchor and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-menubar-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

One action. Sets role="menuitem"; focus follows the pointer. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
onSelect() => voidRuns when the item is activated with a click, Enter, or Space.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-menubar-item]Present
[data-highlighted]Present while focused
[data-disabled]Present when disabled

An item holding a checked state. Sets role="menuitemcheckbox" and aria-checked. Renders its own check indicator, shown while the item is checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
checkedSignal<boolean> | booleanfalseThe checked state; selecting toggles it. Pass a signal to control it from outside.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-menubar-checkbox-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

Wraps radio items and owns which one is checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<string | null> | string | nullnullThe checked item. Pass a signal to control it from outside.
Data attributeValue
[data-menubar-radio-group]Present

One choice in a radio group. Sets role="menuitemradio" and aria-checked. Renders its own dot indicator, shown while the item is selected. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
value*stringIdentifies the item. Must be unique within the radio group.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-menubar-radio-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

A nested menu. Wraps a sub trigger and its sub content inside a parent content.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.

The item that opens its submenu — on hover after openDelay, ArrowRight, Enter, Space, or click. Sets aria-haspopup and aria-expanded. The styled trigger appends a chevron pointing into the submenu. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
openDelaynumber100How long the pointer must rest on the trigger before the submenu opens, in milliseconds.
disabledSignal<boolean> | booleanfalsePrevents opening the submenu; the keyboard skips the item.
Data attributeValue
[data-menubar-sub-trigger]Present
[data-menubar-item]Present
[data-state]"open" | "closed"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

A submenu's panel. Styled like the content panel but without the transition — a submenu should feel instant. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""right"Preferred side of the sub trigger to place the panel.
align"start" | "center" | "end""start"How the panel aligns along the chosen side.
offsetnumber8Distance in pixels between the sub trigger and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-menubar-sub-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

Wraps related items in role="group", labeled by the heading placed inside it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-menubar-group]Present

Names the group it sits in; the group points aria-labelledby at it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-menubar-group-heading]Present

A role="separator" line between sections. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-menubar-separator]Present

The core Portal helper, for rendering the panel into another DOM parent to escape overflow and stacking.