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 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> with role="menuitem" and aria-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> when item.href is set, otherwise a <button data-id> that dispatches apogee:menu on click.
      • Separator — an <hr>, rendered when item.separator is true (other fields on that item are ignored).

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 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 / Space on 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-visible ring 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 trigger role="menuitem" with aria-haspopup="menu" (the browser sets aria-expanded); panel role="menu" named by its label; items role="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 id across menus if you handle apogee:menu globally — give every action item a distinct id.

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…”).