Menu Button
stimeo--menu
A true role=menu widget with roving focus over commands. Distinct from the disclosure dropdown.
The stimeo--menu controller implements the WAI-ARIA Menu Button pattern: a button that opens a role=menu of commands with roving focus across role=menuitem children. Unlike stimeo--dropdown (a disclosure for arbitrary content), it manages arrow-key navigation, Home/End, and closes on Esc, Tab, outside click, or item activation. Static placement is owned by CSS; viewport-edge positioning is intentionally out of scope.
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. |
| 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
type="button"
class="menu__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>
<%# role="menu". Each item is a role="menuitem" button (tabindex=-1). %>
<ul id="menu-list" class="menu__list" role="menu" data-stimeo--menu-target="menu" hidden>
<li role="none">
<button type="button" role="menuitem" class="menu__item" tabindex="-1"
data-stimeo--menu-target="item"
data-action="click->stimeo--menu#activate
keydown->stimeo--menu#onItemKeydown"><%= t(
"components.menu.demo.edit"
) %></button>
</li>
<li role="none">
<button type="button" role="menuitem" class="menu__item" tabindex="-1"
data-stimeo--menu-target="item"
data-action="click->stimeo--menu#activate
keydown->stimeo--menu#onItemKeydown"><%= t(
"components.menu.demo.duplicate"
) %></button>
</li>
<li role="none">
<button type="button" role="menuitem" class="menu__item" tabindex="-1"
data-stimeo--menu-target="item"
data-action="click->stimeo--menu#activate
keydown->stimeo--menu#onItemKeydown"><%= t(
"components.menu.demo.delete"
) %></button>
</li>
</ul>
</div>
/*
* Presentation-only styles for the menu (Menu Button) demo.
* Static placement (showing below the trigger) is this CSS's job. Dynamic collision
* avoidance is not the library's responsibility (a future shared positioning module
* will handle it).
*/
.menu {
position: relative;
display: inline-block;
}
.menu__trigger {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid var(--border-interactive);
border-radius: 0.375rem;
background: var(--surface-card);
}
.menu__trigger[aria-expanded="true"] {
border-color: var(--accent);
}
.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);
}
.menu__item {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.95rem;
color: var(--color-text);
background: transparent;
border: 0;
border-radius: 0.25rem;
cursor: pointer;
}
.menu__item:hover,
.menu__item:focus {
background: var(--surface-subtle);
}
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 role="menu" element shown/hidden via hidden. |
data-stimeo--menu-target="menu" |
item
|
A role="menuitem" command with roving focus; activating it closes the menu and returns focus to the trigger. |
data-stimeo--menu-target="item" |
Actions
| Name | Description | Action |
|---|---|---|
activate
|
Closes the menu after an item is activated and returns focus to the trigger. | 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. |