Menubar
Lab Available in release assets
Prototype the bounded ARIA composite keyboard model for desktop-style command sets when ordinary navigation cannot express the task.
Menubar primary example
Show code
<nav aria-label="Editor commands">
<ul class="thin-menubar" role="menubar">
<li>
<a role="menuitem" href="/editor/file/" aria-haspopup="true" aria-expanded="false">File</a>
<ul role="menu">
<li><a role="menuitem" href="/editor/new/">New document</a></li>
<li><a role="menuitem" href="/editor/open/">Open…</a></li>
</ul>
</li>
<li><a role="menuitem" href="/editor/edit/">Edit</a></li>
<li><button role="menuitem" type="button" aria-disabled="true">Publish unavailable</button></li>
<li><a role="menuitem" href="/editor/help/">Help</a></li>
</ul>
</nav>Variant gallery
Each authored example uses the production assets and includes copyable markup.
Link command destinations
Evaluate the bounded composite keyboard model with real documentation destinations. Explicit enhancement sets roving focus. Ordinary site navigation should use plain links.
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="link-commands" role="menubar" aria-label="Documentation commands">
<li role="none"><a id="link-commands-components" role="menuitem" href="/components/">Components</a></li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="link-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="link-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="link-commands" role="menubar" aria-label="Documentation commands">
<li role="none"><a id="link-commands-components" role="menuitem" href="/components/">Components</a></li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="link-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="link-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Runnable application commands
Focus editor really focuses the labelled field; Open help opens a native dialog with a working close button. The application supplies command handlers separately from the optional keyboard enhancement.
<section style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" role="menubar" aria-label="Editor commands">
<li role="none"><button id="focus-editor" role="menuitem" type="button">Focus editor</button></li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><button id="open-help" role="menuitem" type="button">Open help</button></li>
</ul>
<p>These two example commands run only in this document. Publishing is unavailable.</p>
<label for="demo-editor">Example editor</label>
<textarea id="demo-editor" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">Editable example text.</textarea>
<dialog id="command-help" aria-labelledby="command-help-title" style="box-sizing: border-box; max-inline-size: calc(100% - 2rem); overflow-wrap: anywhere">
<h2 id="command-help-title">Example command help</h2>
<p>Focus editor moves focus to the editable field. No document is saved.</p>
<form method="dialog">
<button style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Close help</button>
</form>
</dialog>
<script type="module">import { enhanceMenubars } from "/labs.js"; enhanceMenubars(document); document.getElementById("focus-editor").addEventListener("click", () => document.getElementById("demo-editor").focus()); document.getElementById("open-help").addEventListener("click", () => document.getElementById("command-help").showModal());</script>
</section>Show code
<section style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" role="menubar" aria-label="Editor commands">
<li role="none"><button id="focus-editor" role="menuitem" type="button">Focus editor</button></li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><button id="open-help" role="menuitem" type="button">Open help</button></li>
</ul>
<p>These two example commands run only in this document. Publishing is unavailable.</p>
<label for="demo-editor">Example editor</label>
<textarea id="demo-editor" style="box-sizing: border-box; inline-size: 100%; min-inline-size: 0">Editable example text.</textarea>
<dialog id="command-help" aria-labelledby="command-help-title" style="box-sizing: border-box; max-inline-size: calc(100% - 2rem); overflow-wrap: anywhere">
<h2 id="command-help-title">Example command help</h2>
<p>Focus editor moves focus to the editable field. No document is saved.</p>
<form method="dialog">
<button style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Close help</button>
</form>
</dialog>
<script type="module">import { enhanceMenubars } from "/labs.js"; enhanceMenubars(document); document.getElementById("focus-editor").addEventListener("click", () => document.getElementById("demo-editor").focus()); document.getElementById("open-help").addEventListener("click", () => document.getElementById("command-help").showModal());</script>
</section>Single-level submenu
Down or activation on Components opens its submenu. Up/Down navigate children; Escape returns to the trigger. Before scripts run, the authored component links stay expanded.
Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="submenu-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="submenu-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="submenu-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="submenu-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="submenu-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Anatomy
- Named navigation fallback
- Menubar composite
- Roving menuitems
- Optional submenu
- Durable link destinations
- Disabled state
Variants and sizes
- Links — shown above
- Command buttons — shown above
- Single-level submenu — shown above
States and difficult content
Rendered fixtures exercise authored content and constrained layouts with the same production assets.
Initial focus, roving and unavailable item
Initially Components is the single enabled root Tab stop. Arrows wrap among enabled items, Home/End select ends, and the native disabled publishing command is skipped. Labels remain readable at narrow widths and enlarged text.
Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="initial-commands" role="menubar" aria-label="Documentation commands">
<li role="none"><a id="initial-commands-components" role="menuitem" href="/components/">Components</a></li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="initial-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="initial-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Open submenu and Escape return
Focus Components and press Down to open the real submenu, then move among its links. Escape closes it and returns focus to Components. Orthogonal root keys do not move focus.
Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="open-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="open-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="open-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="open-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="open-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>RTL composite direction
At the RTL root, Left advances through the authored commands and Right reverses. Inside the bounded submenu, Up/Down still navigate and Right closes toward the trigger.
<nav aria-label="Lab documentation commands" dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="rtl-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="rtl-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="rtl-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="rtl-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="rtl-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Show code
<nav aria-label="Lab documentation commands" dir="rtl" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="rtl-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="rtl-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="rtl-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="rtl-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="rtl-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); </script>Refresh after HTML replacement
Refresh replaces sample child markup and calls refresh(). It is an explicit application simulation, without a server request. Focus within replaced commands is reacquired by stable identity; focusing and activating the refresh button keeps focus on that outside control.
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="morph-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="morph-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="morph-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="morph-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="morph-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<button type="button" id="morph-commands-refresh" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Refresh sample markup</button>
<p id="morph-commands-status" role="status" style="overflow-wrap: anywhere"></p>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); document.getElementById("morph-commands-refresh").addEventListener("click", () => { const bar = document.getElementById("morph-commands"); bar.innerHTML = bar.innerHTML; controller.refresh(); document.getElementById("morph-commands-status").textContent = "Sample markup replaced and behavior refreshed."; });</script>Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="morph-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="morph-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="morph-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="morph-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="morph-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>
<button type="button" id="morph-commands-refresh" style="box-sizing: border-box; max-inline-size: 100%; min-inline-size: 0; min-block-size: 2.75rem; white-space: normal; padding-inline: .5rem; overflow-wrap: anywhere">Refresh sample markup</button>
<p id="morph-commands-status" role="status" style="overflow-wrap: anywhere"></p>
<script type="module">import { enhanceMenubars } from "/labs.js"; const controller = enhanceMenubars(document); document.getElementById("morph-commands-refresh").addEventListener("click", () => { const bar = document.getElementById("morph-commands"); bar.innerHTML = bar.innerHTML; controller.refresh(); document.getElementById("morph-commands-status").textContent = "Sample markup replaced and behavior refreshed."; });</script>Native expanded link fallback
This fixture deliberately omits enhancement. All authored links, including submenu destinations, remain visible and usable in document order. Command-only behavior requires an application handler; durable links remain useful without JavaScript.
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="native-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="native-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="native-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="native-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="native-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>Show code
<nav aria-label="Lab documentation commands" style="min-inline-size: 0; overflow-wrap: anywhere">
<ul class="thin-menubar" id="native-commands" role="menubar" aria-label="Documentation commands">
<li role="none">
<a id="native-commands-components" role="menuitem" href="/components/">Components</a>
<ul role="menu" aria-label="Form component destinations">
<li role="none"><a id="native-commands-input" role="menuitem" href="/components/forms-and-controls/text-input/">Text input</a></li>
<li role="none"><a role="menuitem" href="/components/forms-and-controls/textarea/">Textarea</a></li>
</ul>
</li>
<li role="none"><button role="menuitem" type="button" disabled aria-disabled="true">Publish unavailable</button></li>
<li role="none"><a id="native-commands-layouts" role="menuitem" href="/layouts/stack/">Layouts</a></li>
<li role="none"><a id="native-commands-catalog" role="menuitem" href="/catalog/">Catalog</a></li>
</ul>
<p>Publishing is unavailable in this documentation example. For ordinary navigation, prefer plain links rather than a composite menubar.</p>
</nav>- Initial — shown above
- Roving focus — shown above
- Submenu open — shown above
- Disabled item — shown above
- Escape return — shown above
- Right-to-left — shown above
- Reconnect after morph — shown above
- No JavaScript fallback — shown above
Interaction and keyboard behavior
Lab behavior adds roving focus, Arrow keys, Home, End, submenu entry, and Escape return. Refresh after server morphs. Authored links and expanded submenu content remain useful before enhancement.
Accessibility
Use this Lab only for a genuine composite command surface. Prefer ordinary nav links otherwise; preserve visible labels, durable href fallbacks, aria-disabled semantics, and never trap focus.
- Contract
MENUBAR· APG pattern- Basis
- APG Menu and Menubar for the genuine desktop command composite; explicitly Lab
- Behavior guarantees
- The one genuine menu composite; ordinary navigation, context menus, and command results keep native links, details, dialogs, or forms.
- Synchronizes aria-haspopup, aria-expanded, and aria-controls, with horizontal menubar and vertical submenu arrow keys.
- Skips disabled menu items, restores matching focus through refresh, and restores authored attributes on disconnect.
- Evidence
tests/unit/library/menubar.test.tstests/unit/library/advanced-component-contracts.test.ts
API
Use ul[role=menubar].thin-menubar, direct role=menuitem descendants, and an adjacent role=menu for a bounded submenu. Call refresh after HTML-over-the-wire replacement.
Behavior tier: dependency-free
Structured host, property, and example tables are not authored for this item yet. Use the summary above and the primary example.
Fallback
Before enhancement, every link remains reachable in document order and submenu destinations remain expanded in normal flow.
Assets and dependencies
/foundation.css/labs.css/labs.js
Dependencies: None
Source: packages/ui/src/items/menubar/style.ts