Menu
A trigger and a panel. Click to open; outside click, Escape or choosing an item closes it; the arrow keys move between items. The menu / menuitem ARIA and focus return are handled for you.
The markup contract
A _78-menu-wrap containing a trigger with aria-haspopup="true" and a
_78-menu panel. Give the trigger an id and point the panel at it with
aria-labelledby, so the menu is announced with the name of the thing that opened it.
<div class="_78-menu-wrap">
<button class="_78-btn" id="m-1" type="button" aria-haspopup="true">
Actions
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="_78-menu" aria-labelledby="m-1">
<a class="_78-menu-item" href="#basics">Duplicate</a>
<a class="_78-menu-item" href="#basics">Move to…</a>
<div class="_78-menu-sep"></div>
<a class="_78-menu-item" href="#basics">Archive</a>
</div>
</div>
Items can be <a> or <button>. Use a link when it navigates and
a button when it acts — the menu treats both the same.
Item variants
An inline <svg> as the item's first child is picked up as an icon slot: sized to
16px and dimmed, so a menu of icons stays quiet until hovered. _78-danger on an item
colors it and its icon with --danger, for the one destructive entry.
_78-menu-sep is a hairline divider.
<div class="_78-menu-wrap">
<button class="_78-btn" id="m-2" type="button" aria-haspopup="true">
File
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="_78-menu" aria-labelledby="m-2">
<button class="_78-menu-item" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
New file
</button>
<button class="_78-menu-item" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5h10"/></svg>
Duplicate
</button>
<div class="_78-menu-sep"></div>
<button class="_78-menu-item _78-danger" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg>
Delete
</button>
</div>
</div>
Alignment
A menu opens right-aligned to its trigger, which is what you want for a control near the right edge
of a topbar. _78-menu-left flips it to open leftward, for a trigger on the left of the
screen where the default would run off.
<div class="_78-menu-wrap">
<button class="_78-btn" id="m-3" type="button" aria-haspopup="true">Opens leftward</button>
<div class="_78-menu _78-menu-left" aria-labelledby="m-3">
<a class="_78-menu-item" href="#alignment">Rename</a>
<a class="_78-menu-item" href="#alignment">Export</a>
</div>
</div>
JavaScript API
Menus present at load mount themselves. Mount one you built later, and close any that are open — useful before navigating, or when a keyboard shortcut takes over.
// Menus live on _78.shell, alongside the rest of the app-shell behavior.
_78.shell.mountMenu(wrapEl); // mount one ._78-menu-wrap
_78.shell.mountMenus(scope); // mount every unmounted menu inside scope
_78.shell.closeMenus(); // close whatever is open
| Member | What it is |
|---|---|
_78.shell.mountMenu(wrap) | Wire one _78-menu-wrap. |
_78.shell.mountMenus(scope) | Wire every unmounted menu inside scope (default document). |
_78.shell.closeMenus() | Close all open menus. |
_78.shell because that is where the
account menu in the topbar needed it, but it is a general dropdown you can use anywhere.