Skip to content

Menu

Terminal-styled selection list with keyboard navigation, highlighting and optional hotkey indicators.

Preview

Installation

$pnpm exec dgadd add ui/menu
[Installs to]src/components/ui/menu[Item]ui/menu

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
import { Menu, MenuDivider, MenuItem } from "@/components/ui/menu";export default function MenuDefault() {  return (    <div className="w-64 border border-border">      <Menu aria-label="File actions">        <MenuItem id="new" hotkey={1}>          New File        </MenuItem>        <MenuItem id="open" hotkey={2}>          Open File        </MenuItem>        <MenuItem id="save" hotkey={3}>          Save        </MenuItem>        <MenuDivider />        <MenuItem id="delete" variant="danger">          Delete        </MenuItem>      </Menu>    </div>  );}

Examples

Detail Variant

Preview

Grouped with Labels

Preview

Checkbox and Radio Items

Preview

Custom Icons

Preview

Disabled Items

Preview

Submenu

Preview

Submenu (drill-down stack)

Preview

Controlled Keyboard Navigation

Preview

API Reference

NameTypeDefaultDescription
selectedIdstring | nullControlled selected item id. Pair with onSelect. Switches item role to "menuitemradio" with aria-checked.
defaultSelectedIdstring | nullnullInitial selected id for uncontrolled mode. Setting this to a non-null value enables selection semantics.
highlightedstring | nullControlled highlighted (focused) item id. Pair with onHighlightChange.
defaultHighlightedstring | nullnullInitial highlighted id for uncontrolled mode.
onSelect(id: string) => voidFired when an item is activated by click, Enter, or Space.
onHighlightChange(value: string | null) => voidFired when the highlighted item changes via arrow keys, typeahead, or mouse.
onClose() => voidFired when Escape or Tab is pressed.
variant"default" | "detail""default"Visual layout. `detail` renders taller rows with a right-aligned value column, for menus where each item carries a status or summary value.
wrapbooleantrueWhen true, arrow navigation wraps from last item to first and vice versa.
typeaheadbooleantrueEnable type-ahead character search to jump to matching items. Submenus inherit this setting. Disable when every printable key the menu advertises belongs to an external hotkey layer.
autoFocusbooleanAuto-focus the menu container on mount so arrow keys work without an explicit click.
aria-labelstringAccessible name for the menu container (role="menu").
childrenrequiredReactNodeMenuItem and MenuDivider children.
NameTypeDefaultDescription
idrequiredstringStable identifier matched against selectedId/highlighted and passed to onSelect.
disabledbooleanfalseDisables activation while keeping the item in the navigation order with aria-disabled.
variant"default" | "danger""default"Danger applies destructive coloring for destructive actions.
hotkeynumber | stringDecorative hotkey label rendered as [n]. Does not bind a key listener.
valueReactNodeDetail variant only. Right-aligned value (badge, count, or status text).
valueVariant"default" | "success" | "success-badge" | "muted""default"Color treatment for the detail value. The success variants prefix a ✓ so the passing state does not rest on color alone.
iconReactNodeLeading icon rendered in the indicator slot. Replaces the default ▌/> indicator when provided.
childrenrequiredReactNodeItem label.
NameTypeDefaultDescription
classNamestringClass applied to the separator. Renders role="separator" with horizontal orientation.
NameTypeDefaultDescription
labelReactNodeOptional label rendered via MenuLabel. When provided, the group is labelled via aria-labelledby.
childrenrequiredReactNodeMenuItem, MenuItemCheckbox, MenuItemRadio, or MenuDivider children.
NameTypeDefaultDescription
childrenrequiredReactNodeLabel text for a MenuGroup.
NameTypeDefaultDescription
idrequiredstringStable identifier for the checkbox item.
checkedbooleanControlled checked state.
defaultCheckedbooleanfalseInitial checked state for uncontrolled mode.
onChange(checked: boolean) => voidFired when the checked state toggles.
disabledbooleanfalseDisables the checkbox item.
childrenrequiredReactNodeCheckbox item label.
NameTypeDefaultDescription
idrequiredstringStable identifier for the radio item.
disabledbooleanfalseDisables the radio item.
childrenrequiredReactNodeRadio item label.
NameTypeDefaultDescription
openbooleanControlled open state for the submenu.
defaultOpenbooleanfalseInitial open state for uncontrolled mode.
onOpenChange(open: boolean) => voidFired when the submenu open state changes.
mode"flyout" | "stack" | "auto""auto"Presentation. "flyout" opens a side-anchored panel; "stack" drills down inside the parent panel with a back row. "auto" picks stack on touch or below 640px, where a side flyout has nowhere to go.
childrenrequiredReactNodeMenuSubTrigger and MenuSubContent children.
NameTypeDefaultDescription
idrequiredstringStable identifier for the submenu trigger item.
disabledbooleanfalseDisables the submenu trigger.
childrenrequiredReactNodeTrigger label.
NameTypeDefaultDescription
childrenrequiredReactNodeMenu items rendered inside the submenu floating panel.
sideOffsetnumber0Offset from the trigger edge in pixels.

Data attributes

AttributeApplies toValuesDescription
data-highlightedMenuItem / MenuItemCheckbox / MenuItemRadio / MenuSubTriggerpresent when highlightedMarks the active descendant for keyboard highlight styling, including disabled items that remain discoverable but cannot activate.
data-hoveredMenuItem / MenuItemCheckbox / MenuItemRadio / MenuSubTrigger / MenuStackBackpresent when pointer-hoveredCosmetic pointer hover styling hook. It never enters the accessibility tree and does not move the active descendant.
data-stateMenuItemCheckbox / MenuItemRadio"checked" | "unchecked"Enumerated check state for styling hooks in copy mode.
data-selectedMenuItem / MenuItemRadiopresent when selectedMarks the selected item in selection/radio menu modes.
data-valueMenuItem / MenuItemCheckbox / MenuItemRadio / MenuSubTriggeritem idStable item id used by keyboard navigation and typeahead.
data-diffgazer-navigation-itemMenu items"true"Marks descendants discoverable by @diffgazer/keys navigation utilities.

Accessibility

Keyboard Navigation

Keyboard navigation is built-in. The Controlled Keyboard Navigation example above demonstrates controlled mode with explicit state management. Arrow keys and their vim aliases j/k move the highlight, Enter activates selection.

KeyAction
ArrowUp / kMoves highlight to the previous item. Disabled items remain navigable and announced but cannot be activated.
ArrowDown / jMoves highlight to the next item. Disabled items remain navigable and announced but cannot be activated.
Home / EndMoves highlight to the first or last item, including disabled items that remain nonactivatable.
Enter / SpaceActivates the highlighted item or toggles checkbox/radio items.
Printable characterStarts or extends a typeahead query and moves the highlight to the matching item. j/k still move the highlight on an empty query buffer and only extend a query already in progress.
Escape / TabCalls onClose on the root menu.
ArrowRightOpens the highlighted nested submenu. On a leaf item it does nothing and stays within the current submenu.
ArrowLeft / EscapeCloses submenu content and returns focus to its trigger.

Notes

Composition Contract

Use Menu.Item and Menu.Divider as explicit children in the Menu JSX tree. Custom item UI belongs inside Menu.Item. Components that create items internally from an opaque wrapper are not part of the current public contract.

Row Columns

A default row is icon, label, accelerator. The label column starts immediately after the icon and the [n] accelerator is pushed to the row end, so rows with and without an accelerator keep their labels aligned. Detail rows keep the same shape with a right-aligned value instead of an accelerator.

Submenu presentation

Menu.Sub takes mode="flyout" | "stack" | "auto" (default "auto"). A flyout is a side-anchored panel. A stack is a drill-down: the submenu replaces the item list inside the SAME panel, at the same width and left edge, with a sticky back row that is both the breadcrumb and the pop control. "auto" resolves to stack at coarse pointer or below 640px, resolved before the submenu ever opens so touch never sees a flyout frame — a side flyout has nowhere to go on a narrow screen and ends up shifted back over the rows it came from, hiding them. The key model is identical in both: ArrowRight/Enter pushes, ArrowLeft/Escape pops, and Escape at the root level still closes the menu. While drilled in, the parent items are removed from the DOM tree that navigation reads, so arrow keys and typeahead never reach a row the user cannot see.

Built-in Keyboard Navigation

Menu includes keyboard navigation via useListbox (Arrow keys and their vim aliases j/k, Home/End, Enter/Space). For custom key bindings or cross-component navigation, use the highlighted, onHighlightChange, and onKeyDown props to add external handlers alongside the built-in behavior.

Source

Install via CLI: pnpm exec dgadd add ui/menu. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.