Menu Button
stimeo--menu
A real menu of commands you walk with the arrow keys. Not the same thing as the dropdown.
A menu of commands opened from a button. It implements the WAI-ARIA Menu Button pattern: the arrow keys walk the items and Home and End jump to the ends. Esc, Tab, a click outside, or activating an item closes it. Unavailable items come in two kinds. A fully disabled item is skipped by the arrow keys, while an item marked unavailable stays reachable so it can be found, but cannot be activated. Clicks and keys are handled for the menu as a whole, so items need no setup of their own and items added later work straight away. Placement here is this demo's CSS.
Keyboard
| Key | Action |
|---|---|
| Enter / Space / ↓ | Open the menu and focus the first item. |
| ↑ | Open the menu and focus the last item. |
| ↓ / ↑ | Move focus between items (wrapping). |
| Home / End | Focus the first / last item. |
| Enter / Space on an item | Activate an enabled item; an aria-disabled item remains inactive. |
| Esc | Close the menu and return focus to the trigger. |
<%# Markup for the menu (APG Menu Button) demo.
stimeo--menu is a full role="menu"/"menuitem" menu providing arrow-key roving
focus, Esc, and outside click (distinct from the disclosure dropdown).
Positioning (static placement) is the Playground's CSS. %>
<div class="menu" data-controller="stimeo--menu">
<button
id="menu-trigger"
type="button"
class="demo-trigger"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menu-list"
data-stimeo--menu-target="trigger"
data-action="click->stimeo--menu#toggle keydown->stimeo--menu#onTriggerKeydown">
<%= t("components.menu.demo.trigger") %>
</button>
<%# The trigger labels the menu; each item is a role="menuitem" button (tabindex=-1).
Items need no data-action: the controller handles their clicks and keys from its own
element, so items added or moved in later work without being wired up. %>
<ul id="menu-list" class="menu__list" role="menu" aria-labelledby="menu-trigger"
data-stimeo--menu-target="menu" hidden>
<li role="none">
<button type="button" role="menuitem" class="demo-menuitem" tabindex="-1"
data-stimeo--menu-target="item"><%= t("components.menu.demo.edit") %></button>
</li>
<li role="none">
<button type="button" role="menuitem" class="demo-menuitem" tabindex="-1"
data-stimeo--menu-target="item"><%= t("components.menu.demo.duplicate") %></button>
</li>
<li role="none">
<button type="button" role="menuitem" class="demo-menuitem" tabindex="-1"
data-stimeo--menu-target="item"><%= t("components.menu.demo.delete") %></button>
</li>
</ul>
</div>
/*
* Presentation-only styles for the menu (Menu Button) demo.
* This demo uses static CSS placement below the trigger. Consumers that need
* viewport-aware collision handling can compose the menu with the opt-in
* stimeo-ui/positioning entrypoint.
*/
.menu {
position: relative;
display: inline-block;
}
.menu__list {
position: absolute;
left: 0;
margin: 0.25rem 0 0;
padding: 0.25rem;
list-style: none;
min-width: 12rem;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--menu"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
The menu button that toggles the menu; carries aria-haspopup/aria-expanded and receives focus back on close. |
data-stimeo--menu-target="trigger" |
menu
required
|
The named role="menu" element shown/hidden via hidden; use aria-labelledby to reference the trigger or provide aria-label. |
data-stimeo--menu-target="menu" |
item
|
A role="menuitem" command with roving focus; aria-disabled=true items are skipped and cannot activate. |
data-stimeo--menu-target="item" |
Actions
| Name | Description | Action |
|---|---|---|
activate
|
Closes the menu after an enabled item is activated and returns focus to the trigger; aria-disabled activation is blocked. | stimeo--menu#activate |
close
|
Closes the menu and reflects the collapsed state on the trigger. | stimeo--menu#close |
onItemKeydown
|
Roving focus and closing keys inside the menu: Arrow keys (wrapping), Home/End, Escape (close+focus trigger), Tab (close). | stimeo--menu#onItemKeydown |
onTriggerKeydown
|
Opens the menu from the trigger via keyboard: ArrowDown focuses the first item, ArrowUp the last. | stimeo--menu#onTriggerKeydown |
open
|
Opens the menu and reflects the expanded state on the trigger. | stimeo--menu#open |
toggle
|
Toggles the menu; when opening, focuses the first item. | stimeo--menu#toggle |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-expanded |
Trigger | "true" while the menu is open, "false" when closed. |
hidden |
Menu | Removed when open, present when closed. |