Menu

Actions in an anchored popover — a menu holds actions you run; options you pick from belong to Select, command-and-search to CommandMenu. The panel uses radius.container around radius.nested rows; with a space.1 inset, the curves keep an even offset. Its action rows and section labels use SidebarNav's dense navigation metrics and typography so the same application-level choices speak one visual language.

Item states

Real DOM drivers set every state; the demo class forces focus here. Pointer and keyboard share one highlight, exactly like Select's options.

ExampleStateDriverRolesUsage
Resttext.primaryTransparent row on the panel; the leading icon is optional
Highlighted:hover[data-highlighted]control.bg-hoverPointer and keyboard share one highlight
Focus:focus-visiblefocus.ringKeyboard focus — the shared ring
Disabled[aria-disabled]action.disabled.textVisible but inert — explain why nearby, never vanish the action
Danger--dangeraction.danger.fgDestructive action — red text at rest; danger is intent, not feedback
Danger highlighted:hover[data-highlighted]action.danger.bg-subtleThe danger tint replaces the neutral highlight
Danger disabled--danger[aria-disabled]action.disabled.textDisabled flattens even danger — an inert row never shows the danger tint

Anatomy

Every canvas is wired: the trigger is a button with aria-haspopup="menu" and aria-expanded, its chevron flipping while open — Select's cue. The panel uses radius.container and the rows use radius.nested; their space.1 inset makes the inner and outer curves concentric. Rows match SidebarNav at size.control.sm, font.size.copy-sm, and space.1-5 / space.3 padding. The longest row drives the panel width, with the trigger and container-sm / 4 as floors and a space.4 viewport gutter on each side as the cap. Labels never wrap. The panel opens one space.1 below the trigger, start-aligned, and flips above on viewport collision.

Menu · Default

Label-only rows are the norm — verb + noun in sentence case, nothing else, like buttons (Invite member, not Members).

Menu · Leading icons

All rows carry one or none do — an iconed row next to a bare one misaligns labels. The iconed edge does not step in: unlike Button, a list is a rail, so these rows start on the same space.3 as a text-only menu's. The item owns size.icon.sm; icons ride currentColor.

Menu · Trailing slot

The right-aligned trailing slot is per-row, unlike leading icons: a drill-in chevron or a lock on a gated action. It sits on the same right rail as a shortcut or a check mark — same space.3, same glyph box — so no row's trailing edge disagrees with another's. The locked row pairs the lock with a Tooltip carrying the why.

Keyboard shortcuts

Shortcuts document commands that already work. They use semantic kbd content in the trailing slot, with the platform-native display separated from the machine-readable aria-keyshortcuts value.

Menu · Keyboard shortcuts

Shortcuts use the same 14px UI face as the row, at regular weight in text.secondary. font.tracking.wider adds about 1px between chord glyphs so symbols remain distinct. A space.6 minimum separates command and shortcut; there is no keycap fill or border.

Checked items

The row is the control and the checkmark is only its state indicator. Independent settings use menuitemcheckbox; mutually exclusive settings use menuitemradio inside a named group.

Menu · Multiple selection

Each setting toggles independently and the menu remains open for repeated changes. A space.4 minimum separates label and state marker. Every row reserves size.icon.sm at the trailing edge, so unchecked rows never shift.

Menu · Single selection

Exactly one radio item is checked at a time. The visual remains a checkmark—the semantic role, grouping, and aria-checked behavior carry the exclusive-choice meaning.

Menu · Sections

Group related actions under the same label-sm, regular, wide-tracking recipe used by SidebarNav. A section label sits on the rail of the rows it labels — one space.3 value, whether those rows lead with an icon or with text, because the rows themselves are one rail. The rail is where slots sit; a row ending in its label keeps the same space.4 before the panel edge that a label keeps before a shortcut or a check, because the panel shrink-wraps and would otherwise leave the longest row flush.

Usage notes

A menu holds actions — rows that do something when picked.

VerdictRule
DoMake the trigger a button with aria-haspopup="menu" and aria-expanded; the panel is role="menu" holding role="menuitem" buttons.
DoLabel rows verb + noun in sentence case, like buttons (Invite member, not Members).
DoKeep leading icons all-or-none per menu; trailing content is per-row.
DoLet the longest row set the preferred width; pass the measured trigger width through --ds-menu-anchor-width. Keep labels to one line—the viewport cap truncates rather than creating a second row.
DoDisable with aria-disabled, never the disabled attribute — the row stays visible, focusable, and announced; the host guards activation. Explain why: a lock in the trailing slot, a Tooltip for the full reason.
DoReserve --danger for destructive actions and pair irreversible ones 1:1 with a confirmation — danger is intent (action.danger.fg), never the error feedback roles.
Don’tPut rows that pick a value in a menu — that is Select; command-and-search is CommandMenu.
Don’tStack duplicate separators or leave a divided section unnamed when its actions need a group label.
Don’tOverride a menu's colors per instance — rebrand at the ramp level, never on one menu.