Menubar
stimeo--menubar
An application menubar: roving top items that each open a role=menu, with typeahead.
The stimeo--menubar controller implements the WAI-ARIA Menubar pattern (single level). The top items form one Tab stop via roving tabindex and the arrow keys move between them; ArrowDown/Enter/Space open a menu (ArrowUp opens it at the last item), the arrow keys then move within the menu (wrapping), Home/End jump to the ends, and printable characters move by typeahead. Pressing ArrowLeft/ArrowRight while a menu is open jumps to the adjacent top menu. Escape closes and returns focus to the owning top item; activating an item, Tab, and an outside click also close. Each top item is linked to its menu by aria-controls/id, so markup order is free. Menu placement is your CSS; dynamic placement is delegated to the opt-in stimeo-ui/positioning.
Keyboard
| Key | Action |
|---|---|
| → / ← | Move between top items (wrapping); open the adjacent menu when one is open. |
| ↓ / Enter / Space | Open the focused top item's menu at the first item (ArrowUp: last item). |
| ↓ / ↑ | Move within the open menu (wrapping). |
| Home / End | Move to the first / last item. |
| Printable characters | Typeahead to the next matching item in the open menu. |
| Esc | Close the menu and return focus to its top item. |
<%# Markup for the menubar demo.
Top-level items directly under role="menubar" each open a role="menu". The library
handles roving between top items (left/right keys), opening a menu with
ArrowDown/Enter, up/down movement and typeahead within a menu, moving sideways to
an adjacent menu, returning on Escape, and closing on outside click / Tab. Top
items ↔ menus are linked via aria-controls / id. The look is in demo.css. %>
<div
class="menubar"
data-controller="stimeo--menubar"
role="menubar"
aria-label="<%= t("components.menubar.demo.label") %>"
>
<button
type="button"
class="menubar__top"
role="menuitem"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menubar-file"
data-stimeo--menubar-target="top"
data-action="click->stimeo--menubar#toggle keydown->stimeo--menubar#onTopKeydown">
<%= t("components.menubar.demo.menus.file.label") %>
</button>
<ul
id="menubar-file"
class="menubar__menu"
role="menu"
aria-label="<%= t("components.menubar.demo.menus.file.label") %>"
hidden
data-stimeo--menubar-target="menu"
>
<% %w[new open save].each do |action| %>
<li role="none">
<button
type="button"
class="menubar__item"
role="menuitem"
tabindex="-1"
data-stimeo--menubar-target="item"
data-action="click->stimeo--menubar#activate keydown->stimeo--menubar#onItemKeydown">
<%= t("components.menubar.demo.menus.file.items.#{action}") %>
</button>
</li>
<% end %>
</ul>
<button
type="button"
class="menubar__top"
role="menuitem"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menubar-edit"
data-stimeo--menubar-target="top"
data-action="click->stimeo--menubar#toggle keydown->stimeo--menubar#onTopKeydown">
<%= t("components.menubar.demo.menus.edit.label") %>
</button>
<ul
id="menubar-edit"
class="menubar__menu"
role="menu"
aria-label="<%= t("components.menubar.demo.menus.edit.label") %>"
hidden
data-stimeo--menubar-target="menu"
>
<% %w[cut copy paste].each do |action| %>
<li role="none">
<button
type="button"
class="menubar__item"
role="menuitem"
tabindex="-1"
data-stimeo--menubar-target="item"
data-action="click->stimeo--menubar#activate keydown->stimeo--menubar#onItemKeydown">
<%= t("components.menubar.demo.menus.edit.items.#{action}") %>
</button>
</li>
<% end %>
</ul>
</div>
/*
* Presentation-only styles for the menubar demo.
* The library expresses open/close via the menu's hidden, the active top item via
* aria-expanded, and the active in-menu item via focus. Menu placement (directly
* below the top item) is static and the consumer's CSS responsibility; use
* stimeo-ui/positioning for dynamic flip.
*/
.menubar {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--surface-subtle);
/* Positioning context for the menus' absolute placement. */
position: relative;
width: fit-content;
}
.menubar__top {
padding: 0.4rem 0.75rem;
font: inherit;
cursor: pointer;
border: 0;
border-radius: 0.25rem;
background: transparent;
color: var(--color-text);
}
.menubar__top:hover,
.menubar__top[aria-expanded="true"] {
background: var(--surface-subtle);
}
.menubar__menu {
position: absolute;
top: calc(100% + 0.25rem);
z-index: 10;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
.menubar__menu[hidden] {
display: none;
}
.menubar__item {
display: block;
width: 100%;
padding: 0.45rem 0.6rem;
font: inherit;
text-align: left;
cursor: pointer;
border: 0;
border-radius: 0.25rem;
background: transparent;
color: var(--color-text);
}
.menubar__item:hover,
.menubar__item:focus {
background: var(--vital-100);
outline: none;
}
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--menubar"
Targets
| Name | Description | Attribute |
|---|---|---|
top
required
|
A top-level menuitem button; the top items form one Tab stop via roving tabindex. |
data-stimeo--menubar-target="top" |
menu
required
|
A role=menu popup linked to its top item by aria-controls/id; shown when open. |
data-stimeo--menubar-target="menu" |
item
required
|
A menuitem inside a menu; arrow keys and typeahead move focus among them. | data-stimeo--menubar-target="item" |
Actions
| Name | Description | Action |
|---|---|---|
activate
|
Closes the menus after an item is activated and refocuses the owning top item. | stimeo--menubar#activate |
onItemKeydown
|
Handles keyboard while focus is on a menu item (arrows, Home/End, adjacent-menu jump, Escape, Tab, typeahead). | stimeo--menubar#onItemKeydown |
onTopKeydown
|
Handles keyboard while focus is on a top item (arrows move/open, Home/End, Escape closes). | stimeo--menubar#onTopKeydown |
toggle
|
Toggles the clicked top item's menu open or closed. | stimeo--menubar#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 |
Top item | Open/closed state of that menu. |
tabindex |
Top item | 0 on the active top item, -1 on the rest (roving). |
hidden |
Menu | Present when closed. |