Menu
Manual until a YAML contract lands in
monochrome/spec/components/.
Dropdown menu with optional submenus, checkbox items, and radio items. Uses the Popover API (popover="manual"). Ids: mct:menu:<id> / mcc:menu:<id>. Trigger declares aria-haspopup="menu".
Composition
Root
Trigger
Popover (ul role="menu" popover="manual")
Label | Separator | Item | CheckboxItem | RadioItem | Group
Group → nested Trigger + Popover (submenu)Usage
HTML
<div>
<button type="button" id="mct:menu:account" aria-controls="mcc:menu:account" aria-expanded="false" aria-haspopup="menu">
Account
</button>
<ul role="menu" id="mcc:menu:account" aria-labelledby="mct:menu:account" aria-hidden="true" popover="manual">
<li role="presentation">Settings</li>
<li role="none"><button type="button" role="menuitem" tabindex="-1">Profile</button></li>
<li role="none"><button type="button" role="menuitem" tabindex="-1">Preferences</button></li>
<li role="separator"></li>
<li role="none"><button type="button" role="menuitem" tabindex="-1">Sign Out</button></li>
</ul>
</div>React
import { Menu } from "@monochrome-ui/react"
<Menu.Root>
<Menu.Trigger>Account</Menu.Trigger>
<Menu.Popover>
<Menu.Label>Settings</Menu.Label>
<Menu.Item>Profile</Menu.Item>
<Menu.Item>Preferences</Menu.Item>
<Menu.Separator />
<Menu.Item>Sign Out</Menu.Item>
</Menu.Popover>
</Menu.Root>Vue
<script setup lang="ts">
import { Menu } from "@monochrome-ui/vue"
</script>
<template>
<Menu.Root>
<Menu.Trigger>Account</Menu.Trigger>
<Menu.Popover>
<Menu.Label>Settings</Menu.Label>
<Menu.Item>Profile</Menu.Item>
<Menu.Item>Preferences</Menu.Item>
<Menu.Separator />
<Menu.Item>Sign Out</Menu.Item>
</Menu.Popover>
</Menu.Root>
</template>Parts
Root
Required for wrappers (context). Element: fragment/provider only in React (MenuContext); no obligatory wrapper element in HTML beyond Trigger + Popover.
Trigger
Required. Element: button.
| Kind | Name | Type / value | Default | Description |
|---|---|---|---|---|
| attr | type |
"button" |
||
| attr | id |
mct:menu:<id> |
||
| attr | aria-controls |
mcc:menu:<id> |
||
| attr | aria-expanded |
"true" | "false" |
"false" |
Open state. |
| attr | aria-haspopup |
"menu" |
||
| attr | tabindex |
0 (root) / -1 (submenu trigger) |
||
| attr | role |
"button" (root) / "menuitem" (submenu) |
Wrappers set this from context. | |
| attr | aria-disabled |
"true" | absent |
absent | Blocks open when "true". |
| prop | disabled |
boolean |
false |
Emits aria-disabled. |
Popover
Required. Element: ul.
| Kind | Name | Type / value | Default | Description |
|---|---|---|---|---|
| attr | role |
"menu" |
||
| attr | id |
mcc:menu:<id> |
||
| attr | aria-labelledby |
mct:menu:<id> |
||
| attr | aria-hidden |
"true" | "false" |
"true" |
|
| attr | popover |
"manual" |
Popover API; core calls showPopover / hidePopover. |
Item
| Kind | Name | Type | Default | Description |
|---|---|---|---|---|
| prop | disabled |
boolean |
false |
Renders span with aria-disabled instead of button/link. |
| prop | href |
string |
When set, renders <a role="menuitem"> instead of button. |
Markup: li role="none" wrapping button|a|span with role="menuitem" and tabindex="-1".
CheckboxItem
| Kind | Name | Type | Default | Description |
|---|---|---|---|---|
| prop | checked |
boolean |
false |
Seeds aria-checked. Core toggles on activate. |
| prop | disabled |
boolean |
false |
Inner role: menuitemcheckbox.
HTML:
<li role="none">
<button type="button" role="menuitemcheckbox" aria-checked="false" tabindex="-1">Bold</button>
</li>RadioItem
| Kind | Name | Type | Default | Description |
|---|---|---|---|---|
| prop | checked |
boolean |
false |
Seeds aria-checked. Activating clears adjacent radios in the group, then sets this item. |
| prop | disabled |
boolean |
false |
Inner role: menuitemradio. Radio groups are DOM-adjacent; a Separator or non-radio item breaks the group.
HTML:
<li role="none">
<button type="button" role="menuitemradio" aria-checked="true" tabindex="-1">Left</button>
</li>
<li role="none">
<button type="button" role="menuitemradio" aria-checked="false" tabindex="-1">Center</button>
</li>Label
li role="presentation". Non-interactive section heading inside the menu.
Separator
li role="separator". Also acts as a radio group boundary.
Group
Submenu container. Wrappers: li role="none" providing a new menu id context; nest Trigger + Popover inside.
HTML submenu pattern:
<li role="none">
<button type="button" role="menuitem" id="mct:menu:more" aria-controls="mcc:menu:more"
aria-expanded="false" aria-haspopup="menu" tabindex="-1">More</button>
<ul role="menu" id="mcc:menu:more" aria-labelledby="mct:menu:more" aria-hidden="true" popover="manual">
<!-- items -->
</ul>
</li>Notes
- Open: Trigger
aria-expanded="true", Popoveraria-hidden="false", popover shown. Core positions via CSS vars on the content element. - Outside click / scroll / Escape: Closes (Escape closes one layer per press; submenu first).
- Activation: Plain
menuitemcloses all menus. Checkbox togglesaria-checked. Radio sweeps the group then checks the activated item. - Mouse vs keyboard open: Keyboard open focuses first (or last on ArrowUp) item; mouse open leaves focus on the trigger (
event.detail). - Safety triangle: Submenu hover uses CSS
clip-path+ pointermove vars so diagonal travel does not dismiss the submenu. See Architecture. - Ids: Unique
<id>per Trigger/Popover pair. Items need no monochrome ids (role is enough).
Keyboard
| Key | Context | Behavior |
|---|---|---|
| Enter / Space | Trigger | Open (keyboard open focuses first item) |
| ArrowDown | Closed root trigger | Open, focus first item |
| ArrowUp | Closed root trigger | Open, focus last item |
| ArrowDown / ArrowUp | Inside menu | Roving focus among items |
| ArrowRight | Submenu trigger | Open submenu, focus first item |
| ArrowLeft | Inside submenu | Close submenu, focus its trigger |
| Home / End | Inside menu | First / last item |
| a-z | Inside menu | Typeahead to next matching item |
| Escape | Open menu | Close topmost layer; focus returns to its trigger |
| Tab | Open menu | Close all; leave focus path for next Tab |