Menubar
A desktop-style app menubar (File, Edit, View…) with keyboard navigation between menus.
Last updated
Uses JavaScript
Wires the desktop menubar keys: ← / → move between menus (and switch the
open one), ↓ opens a menu on its first item, arrows move inside a menu;
hovering a sibling while a menu is open switches to it; activating an item
closes the menu and dispatches apogee:menu.
Without JS: Each menu opens and closes natively (popovertarget); opening one closes the others, and Escape or an outside click closes it. Without JS there are no arrow keys, no hover switching, and items don't close the menu.
Show codeHide code
<Menubar menus={[
{
label: 'File',
items: [
{ label: 'New', id: 'new' },
{ label: 'Open…', id: 'open' },
{ separator: true },
{ label: 'Exit', id: 'exit' },
],
},
{
label: 'Edit',
items: [
{ label: 'Undo', id: 'undo' },
{ label: 'Redo', id: 'redo' },
],
},
]} />Add
npx @ava-ui/apogee add menubar
Depends on utils. Ships one scoped JS island (progressive enhancement).
Usage
---
import Menubar from '@/components/apogee/Menubar/Menubar.astro'
---
<Menubar menus={[
{
label: 'File',
items: [
{ label: 'New', id: 'new' },
{ label: 'Open…', id: 'open' },
],
},
]} />
Anatomy
- Menubar root —
role="menubar", a row of top-level menus. - Menu — one wrapper per top-level entry (
menus[i]).- Trigger — a
<button popovertarget>withrole="menuitem"andaria-haspopup="menu"; the visible label. Native open/close, zero JS. - Panel — a
role="menu"popover anchored under the trigger, in the top layer; holds the items.- Item —
role="menuitem"; an<a>whenitem.hrefis set, otherwise a<button data-id>that dispatchesapogee:menuon click. - Separator — an
<hr>, rendered whenitem.separatoristrue(other fields on that item are ignored).
- Item —
- Trigger — a
API
@composable — exposed to Content Editors in AVA. Any other attribute
passes through to the root (role="menubar") div.
| Prop | Type | Default | Description |
|---|---|---|---|
menus |
{ label: string; items: MenuItem[] }[] |
— | Top-level menus, in order. |
class |
string |
— | Extra classes on the root, merged last. |
MenuItem:
| Field | Type | Description |
|---|---|---|
label |
string |
Item text. |
href |
string |
Renders the item as an <a> (navigates). Omit for an action item. |
id |
string |
Passed back in the apogee:menu event detail when a href-less item is clicked. |
separator |
boolean |
Renders a divider instead of an item. |
States
Show codeHide code
<Menubar menus={[
{
label: 'View',
items: [
{ label: 'Zoom in', id: 'zoom-in' },
{ label: 'Zoom out', id: 'zoom-out' },
],
},
]} />- Closed (default) — each popover is closed.
- Open — click, or
Enter/Spaceon a trigger; siblings close natively; the panel fades and scales in (--duration-fast, respects reduced motion). - Hover / focus — triggers and items lighten on hover; a visible
focus-visiblering on triggers. - No JS — see the JsNote above: menus open, close and light-dismiss natively; the keyboard and hover behaviour is JS-only.
Examples
Listening for item activation:
<script>
document.addEventListener('apogee:menu', (e) => {
console.log(e.detail.id, e.detail.label)
})
</script>
Accessibility
| Key | Action |
|---|---|
Tab |
Moves between menu triggers (each is a tab stop). |
Enter Space |
Toggle the focused menu; on an item, activate it and close the menu. |
← → |
Move to the previous / next trigger, wrapping; if a menu is open, open that one instead. Swapped under dir="rtl". |
↓ |
On a trigger, opens its menu and focuses the first item. |
↓ ↑ |
Inside a menu, move between items, wrapping at the ends. |
Home End |
Inside a menu, first / last item. |
Esc |
Closes the menu and returns focus to its trigger. |
role="menubar"root; each triggerrole="menuitem"witharia-haspopup="menu"(the browser setsaria-expanded); panelrole="menu"named by its label; itemsrole="menuitem".- Focus-visible ring on triggers and items.
- No JS — every menu still opens, closes, light-dismisses and closes its siblings (native popovers); the keyboard and hover behaviour is a JS-only enhancement — nothing is hidden.
Tokens
| Token | Used for |
|---|---|
--border-neutral |
Root and panel border |
--background-page |
Root background |
--background-elevation-overlay |
Panel background |
--foreground-neutral |
Trigger and item text |
--background-neutral-faded-highlighted |
Hover / focus background on triggers and items |
--outline-color-focus |
Focus ring |
--duration-fast / --ease-standard |
Panel open animation |
Do / Don’t
- Do keep top-level labels to one word (“File”, “Edit”, “View”) — this is a menubar, not primary navigation.
- Do group related actions with a separator instead of one undivided list.
- Don’t nest a Menubar inside another, or use it for page navigation —
use
Navbar/NavLink. - Don’t reuse an item
idacross menus if you handleapogee:menuglobally — give every action item a distinctid.
Content
- Top-level labels: one word, title case (“File”, “Edit”, “View”).
- Item labels: sentence case, verb-led for actions (“New file”).
- Trailing
…on an item that opens another step before acting (“Open…”).