Navigation Menu
stimeo--navigation-menu
Disclosure navigation: single-open sub-panels of links, not a role=menu.
The stimeo--navigation-menu controller implements the WAI-ARIA Disclosure navigation pattern. Each top-level button toggles its sub-panel (aria-expanded + hidden synced), only one panel is open at a time, and the panel content is plain links (not a role=menu). Focus is not trapped — Tab moves through the links naturally. Escape closes the open panel and returns focus to its trigger; an outside click or focus leaving the nav closes it. ArrowLeft/ArrowRight move focus between triggers without rewriting tabindex (they keep their natural Tab order). Hover open/close is opt-in via openOnHover (with hoverDelay). Panel layout and animation are your CSS; dynamic placement is delegated to the opt-in stimeo-ui/positioning. For an app command menu with arrow roving and role=menu, use the menubar instead.
Keyboard
| Key | Action |
|---|---|
| → / ← | Move focus between top-level triggers (wrapping). |
| Esc | Close the open panel and return focus to its trigger. |
| Tab | Move naturally; closes the open panel when focus leaves the nav. |
<%# Markup for the navigation-menu demo.
A global nav where each top item opens/closes a sub-panel (a set of links). The
contents are links and don't use role="menu" (APG Disclosure navigation). The
library handles syncing aria-expanded, single-open behavior, closing on Escape /
outside click / focus leaving, and moving between top items with the left/right
keys. Panel placement and styling are the consumer's CSS (demo.css). %>
<nav class="nav-menu" data-controller="stimeo--navigation-menu"
aria-label="<%= t("components.navigation_menu.demo.label") %>">
<ul class="nav-menu__list">
<li class="nav-menu__item">
<button
type="button"
class="nav-menu__trigger"
aria-expanded="false"
aria-controls="nav-menu-products"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<%= t("components.navigation_menu.demo.products.label") %>
</button>
<div id="nav-menu-products" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a href="#analytics"><%= t(
"components.navigation_menu.demo.products.links.analytics"
) %></a>
<a href="#automation"><%= t(
"components.navigation_menu.demo.products.links.automation"
) %></a>
<a href="#reports"><%= t("components.navigation_menu.demo.products.links.reports") %></a>
</div>
</li>
<li class="nav-menu__item">
<button
type="button"
class="nav-menu__trigger"
aria-expanded="false"
aria-controls="nav-menu-company"
data-stimeo--navigation-menu-target="trigger"
data-action="click->stimeo--navigation-menu#toggle
keydown->stimeo--navigation-menu#onTriggerKeydown">
<%= t("components.navigation_menu.demo.company.label") %>
</button>
<div id="nav-menu-company" class="nav-menu__panel"
data-stimeo--navigation-menu-target="panel" hidden>
<a href="#about"><%= t("components.navigation_menu.demo.company.links.about") %></a>
<a href="#careers"><%= t("components.navigation_menu.demo.company.links.careers") %></a>
</div>
</li>
</ul>
</nav>
/*
* Presentation-only styles for the navigation-menu demo.
* The library toggles the panel's hidden and the top items' aria-expanded. Panel
* placement (directly below the trigger) is static and the consumer's CSS
* responsibility; use stimeo-ui/positioning for dynamic flip.
*/
.nav-menu__list {
display: flex;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.nav-menu__item {
position: relative;
}
.nav-menu__trigger {
padding: 0.5rem 0.75rem;
font: inherit;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.375rem;
background: transparent;
color: var(--color-text);
}
.nav-menu__trigger:hover,
.nav-menu__trigger[aria-expanded="true"] {
border-color: var(--border-strong);
background: var(--surface-subtle);
}
.nav-menu__panel {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
min-width: 12rem;
padding: 0.5rem;
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);
}
.nav-menu__panel[hidden] {
display: none;
}
.nav-menu__panel a {
padding: 0.4rem 0.5rem;
border-radius: 0.25rem;
color: var(--color-text);
text-decoration: none;
}
.nav-menu__panel a:hover,
.nav-menu__panel a: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--navigation-menu"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
A top-level disclosure button that toggles its sub-panel; aria-expanded is synced. |
data-stimeo--navigation-menu-target="trigger" |
panel
required
|
The sub-panel of links controlled by a trigger (aria-controls/id); its hidden state is synced. |
data-stimeo--navigation-menu-target="panel" |
Values
| Name | Description | Attribute |
|---|---|---|
openOnHover
|
Whether hover opens/closes panels (opt-in); default false. | data-stimeo--navigation-menu-open-on-hover-value |
hoverDelay
|
Delay in ms before hover opens or closes a panel; default 150. | data-stimeo--navigation-menu-hover-delay-value |
Actions
| Name | Description | Action |
|---|---|---|
onTriggerKeydown
|
ArrowLeft/ArrowRight move focus between triggers, keeping their natural Tab order. | stimeo--navigation-menu#onTriggerKeydown |
toggle
|
Toggles the clicked trigger's panel open or closed (single-open). | stimeo--navigation-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 | Open/closed state of that panel (true/false). |
hidden |
Panel | Present when closed. |