rc-menu
Menu popup for command surfaces with keyboard navigation and typed activation events, following the WAI-ARIA Menu pattern.
- Package
@rcarls/rc-menu- Element
<rc-menu>- Native dependency
- Consumer supplies menu item controls
- State model
- Host focus with an active descendant
- Main events
rc-menu-activaterc-menu-close- Related
- Theme previews
Installation
- npm
- Yarn
npm install @rcarls/rc-menu
yarn add @rcarls/rc-menu
import '@rcarls/rc-menu/define';
Live demo
Interaction model
rc-menu is the focus target and owns the menu role. It exposes the active
slotted item through aria-activedescendant instead of moving DOM focus between
items. Arrow Down and Arrow Up move the virtual cursor and wrap at the menu
boundaries; Home and End select the first or last item. Enter, Space, and pointer
interactions activate menu items, while Escape requests that the containing popup
close.
The public focusFirst(), focusLast(), focusItem(), and focusItemAt()
methods follow the same model: each focuses the host and updates the virtual
cursor. The read-only items getter returns the current navigable elements in
document order.
Menu item content
Direct focusable children become menuitem entries. Use
role="menuitemcheckbox" or role="menuitemradio" with aria-checked for
checked choices; activation updates the checked state before the activation
event is dispatched. A labeled role="group" can contain one level of related
items, and <hr> or role="separator" elements divide sections without joining
keyboard navigation.
<rc-menu label="View">
<button type="button" role="menuitemcheckbox" aria-checked="true" value="details">
Show details
</button>
<hr />
<button type="button" value="duplicate">
<span>Duplicate</span>
<kbd data-menu-shortcut>Ctrl+D</kbd>
</button>
</rc-menu>
Nested menu triggers supply their own visual affordance through
rc-menu-button's indicator slot. rc-menu does not generate an indicator,
leaving iconography and writing-direction behavior to the application or theme.
Events
Both events bubble across shadow boundaries and are not cancelable.
| Event | Detail | When |
|---|---|---|
rc-menu-activate | { item, value, text, checked? } | Enter, Space, or a pointer click activates an enabled item |
rc-menu-close | { reason: 'escape' } | Escape requests that the containing popup close |
The activation value comes from data-value or value, text is the item's
trimmed text content, and checked reports the resulting checkbox or radio
state when applicable.
Theming
The default demo mode shows the component without a package theme. Use the shared preview controls on this page to compare inherited, light, and dark color schemes or to apply the optional Material theme only inside the demo frames.
Slotted rows remain in light DOM. rc-menu injects their structural base styles
once per containing document or shadow root under @layer rc-base, so consumer
styles outside that layer can override them. Inherited --rc-menu-* custom
properties customize the host and rows; the component does not expose CSS
parts. Add trailing keyboard hints with [data-menu-shortcut].
API
Properties
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
label | label | string | '' | Accessible label for this menu. Synced to `aria-label` on the host. Prefer `aria-labelledby` for a visible label associated via ID. |
items | JS property only | readonly Element[] | Not specified | All navigable menu item elements in document order. |
Methods
| Method | Description |
|---|---|
focusFirst() | Move the virtual cursor and DOM focus to the first navigable item. |
focusLast() | Move the virtual cursor and DOM focus to the last navigable item. |
focusItem($item?: Element | null) | Move the virtual cursor to a specific item. |
focusItemAt(index: number) | Move the virtual cursor to the item at `index`. |
Events
| Event | Detail type | Description |
|---|---|---|
rc-menu-activate | No detail type documented | Fired when a menu item is activated via keyboard (Enter/Space) or pointer click. |
rc-menu-close | No detail type documented | Fired when Escape is pressed. |
Slots
| Name | Description |
|---|---|
default | Menu items, groups, and separators. Supported content model: - Focusable elements (`<button>`, `<a>`, `[tabindex]`) become navigable `role="menuitem"` entries. - `role="menuitemcheckbox"` / `role="menuitemradio"` elements get a leading indicator column via `::before`; `aria-checked` is initialized when missing and updated on activation. - `role="group"` divs group related items; label them with `aria-label` or `aria-labelledby`. Optionally add a visible `[data-group-label]` element as first child of the group for section headings. - `role="separator"` / `<hr>` produce horizontal dividers between sections. - Within any item, `[data-menu-shortcut]` elements are pushed to the inline-end edge for keyboard hints. - Nested menu triggers supply their own trailing indicator through `rc-menu-button`'s `indicator` slot. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-menu-min-width | 10em | Minimum width of the menu panel. |
--rc-menu-padding-block | 0.25em | Block (top/bottom) padding inside the panel. |
--rc-menu-background | Canvas | Background color; falls back through --rc-surface. |
--rc-menu-border | 1px solid ButtonBorder | Border; falls back through --rc-border. |
--rc-menu-radius | var(--rc-control-radius) | Menu panel border radius. |
--rc-menu-shadow | 0 2px 8px color-mix(in srgb, CanvasText 15%, transparent) | Box shadow; falls back through --rc-shadow. |
--rc-menu-color | CanvasText | Text color; falls back through --rc-field-text. |
--rc-menu-item-min-block-size | 0 | Minimum block size for menu item rows. |
--rc-menu-item-padding-block | var(--rc-item-padding-block) | Menu item block-axis padding. |
--rc-menu-item-padding-inline | var(--rc-item-padding-inline) | Menu item inline-axis padding. |
--rc-menu-item-gap | var(--rc-item-gap) | Gap between flex children inside each item. |
--rc-menu-item-transition | Not specified | CSS transition applied to each item row. |
--rc-menu-hover-bg | color-mix(in srgb, Highlight 8%, transparent) | Pointer-hover background. |
--rc-menu-hover-color | inherit | Pointer-hover text color. |
--rc-menu-active-bg | color-mix(in srgb, Highlight 8%, transparent) | Virtual-cursor active background. |
--rc-menu-active-color | inherit | Virtual-cursor active text color. |
--rc-menu-check-size | 1.25em | Width of the leading indicator column for checkbox/radio items. |
--rc-menu-check-color | inherit | Color of the check/radio symbol. |
--rc-menu-shortcut-size | 0.8em | Font size of `[data-menu-shortcut]` labels. |
--rc-menu-shortcut-color | GrayText | Text color of `[data-menu-shortcut]` labels. |
--rc-menu-disabled-color | GrayText | Disabled menu item text color. |
--rc-menu-disabled-opacity | var(--rc-disabled-opacity) | Disabled menu item opacity. |
--rc-menu-separator-border | 1px solid ButtonBorder | Separator border. |
--rc-menu-separator-margin-block | var(--rc-control-gap) | Separator block-axis margin. |
--rc-menu-separator-margin-inline | 0px | Separator inline margin; set to match item padding for inset separators. |
--rc-menu-group-label-padding-block | 0.1em | Block padding on `[data-group-label]` section headings. |
--rc-menu-group-label-color | GrayText | Text color of `[data-group-label]` section headings. |
CSS Parts
No CSS parts are documented in the custom elements manifest.