Menubar
stimeo--menubar
An application menubar: top items you walk sideways, each opening a menu, with typeahead.
An application menubar, implementing the WAI-ARIA Menubar pattern at a single level. Tab stops on the bar once, and the left and right keys move between the top items. Down, Enter or Space opens a menu, and opening with Up starts at its last item. Inside a menu the up and down keys wrap around, Home and End jump to the ends, and typing a character jumps to the item starting with it. Pressing left or right with a menu open moves to the neighbouring menu. Esc closes and returns focus to the item that owned it, and so do activating an item, Tab, and a click outside. Unavailable and hidden entries are skipped by the arrow keys. Each top item names its menu in the markup, so the markup order is free. Placement here is this demo's CSS.
Keyboard
| Key | Action |
|---|---|
| → / ← | Move between top items (wrapping); open the adjacent menu when one is open. Under RTL the two swap. |
| ↓ / 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; open the first / last menu when one is open. |
| Printable characters | Typeahead to the next matching item; repeat a character to cycle through matches. |
| Enter / Space (menu item) | Run the item and close the menu. |
| Esc | Close the menu and return focus to its top item. |
| Tab | Close the menu; focus moves on to the next element (it is not sent back). |
<%# 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="demo-baritem"
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="demo-menuitem"
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="demo-baritem"
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="demo-menuitem"
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;
}
/*
* The open menu is the same floating surface as the navigation-menu demo's panel
* (same width, padding, border, radius and shadow), so the two menu components
* read as one visual language in the catalog. Only the horizontal anchor differs:
* no `left` is set, which leaves each menu at its static position under its own
* top item instead of the bar's left edge.
*/
.menubar__menu {
position: absolute;
top: calc(100% + 0.25rem);
z-index: 10;
min-width: 12rem;
margin: 0;
padding: 0.5rem;
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;
}
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
|
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, Tab, typeahead). | stimeo--menubar#onItemKeydown |
onTopKeydown
|
Handles keyboard while focus is on a top item (arrows move/open, Home/End, Tab 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. |