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.
Real DOM drivers set every state; the demo class forces focus here. Pointer and keyboard share one highlight, exactly like Select's options.
| Example | State | Driver | Roles | Usage |
|---|---|---|---|---|
| Rest | — | text.primary | Transparent row on the panel; the leading icon is optional | |
| Highlighted | :hover[data-highlighted] | control.bg-hover | Pointer and keyboard share one highlight | |
| Focus | :focus-visible | focus.ring | Keyboard focus — the shared ring | |
| Disabled | [aria-disabled] | action.disabled.text | Visible but inert — explain why nearby, never vanish the action | |
| Danger | --danger | action.danger.fg | Destructive action — red text at rest; danger is intent, not feedback | |
| Danger highlighted | :hover[data-highlighted] | action.danger.bg-subtle | The danger tint replaces the neutral highlight | |
| Danger disabled | --danger[aria-disabled] | action.disabled.text | Disabled flattens even danger — an inert row never shows the danger tint |
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.
Label-only rows are the norm — verb + noun in sentence case, nothing else, like buttons (Invite member, not Members).
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.
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.
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.
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.
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.
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.
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.
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.
A menu holds actions — rows that do something when picked.
| Verdict | Rule |
|---|---|
| Do | Make the trigger a button with aria-haspopup="menu" and aria-expanded; the panel is role="menu" holding role="menuitem" buttons. |
| Do | Label rows verb + noun in sentence case, like buttons (Invite member, not Members). |
| Do | Keep leading icons all-or-none per menu; trailing content is per-row. |
| Do | Let 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. |
| Do | Disable 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. |
| Do | Reserve --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’t | Put rows that pick a value in a menu — that is Select; command-and-search is CommandMenu. |
| Don’t | Stack duplicate separators or leave a divided section unnamed when its actions need a group label. |
| Don’t | Override a menu's colors per instance — rebrand at the ramp level, never on one menu. |