Fylgja Menu
A menu is a short list of actions that drops down from a button, such as the
options for a file or the actions for a table row. It is an
Action List on a native <menu> popover. The Popover API
puts it in the top layer and closes it on Escape or an outside click, and CSS anchor
positioning keeps it attached to its button, so opening, closing, and placing the
menu takes no JavaScript.
Preview
<button type="button" commandfor="file-menu" command="toggle-popover">
<span>File options</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="toggle-rotate transition" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</button>
<menu id="file-menu" popover class="action-menu list-none" role="list">
<li>
<button type="button" commandfor="file-menu" command="hide-popover" class="w-full align" style="--align: center start">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
<span>Copy link</span>
</button>
</li>
<li>
<button type="button" commandfor="file-menu" command="hide-popover" class="w-full align" style="--align: center start">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>
<span>Rename</span>
</button>
</li>
<li>
<button type="button" commandfor="file-menu" command="hide-popover" class="w-full align" style="--align: center start">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></svg>
<span>Download</span>
</button>
</li>
<li><hr style="--my: 0.25rem; border-block-end-width: 1px; border-color: color-mix(in oklab, var(--root-bg) 75%, var(--root-fg))"></li>
<li>
<button type="button" commandfor="file-menu" command="hide-popover" class="w-full align" style="--align: center start; --btn-color: color-mix(in oklab, oklch(58% 0.21 var(--hue-red)) 80%, var(--root-fg))">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
<span>Delete file</span>
</button>
</li>
</menu>
<style>
.action-menu {
--btn-stroke: transparent;
min-inline-size: 12rem;
padding: 0.25rem;
border: 1px solid color-mix(in oklab, var(--root-bg) 75%, var(--root-fg));
border-radius: 0.5rem;
background-color: color-mix(in oklab, var(--root-bg) 88%, white);
color: var(--root-fg);
box-shadow: var(--shadow-2);
@supports (position-area: block-end) {
inset: auto;
margin: 0.25rem 0;
position-area: var(--menu-position, block-end span-inline-end);
position-try-fallbacks: flip-block, flip-inline;
}
}
/* Until the toggle utilities support popovers, see fylgja/fylgja#419 */
[commandfor]:has(+ .action-menu:popover-open) > .toggle-rotate {
rotate: 180deg;
}
</style>Usage
- A trigger
<button>withcommandforandcommand="toggle-popover"opens and closes the menu. The browser also exposes its expanded state to assistive technology. - The menu is a
<menu popover>with the.action-menuclass.<menu>is a list of commands, so the items sit directly in it as<li>elements and the popover needs no extra wrapper.popover(theautostate) gives light dismiss, so Escape or a click outside closes it, and opening one menu closes any other. - Each
<li>holds a button or link. Group items with an<li>that holds an<hr>. Acommand="hide-popover"on an item closes the menu once the action is picked. Links close it by navigating. - The small
.action-menurule draws the panel and positions it. Set the--menu-positionvariable to anyposition-areavalue to change where it opens, such asblock-end span-inline-startfor a trigger at the end of a row.
Anatomy
| Part | Responsibility |
|---|---|
Trigger <button command> |
Toggles the menu and acts as its anchor. |
<menu popover class="action-menu"> |
The panel and the list. Top layer, light dismiss, anchored under the trigger. |
<li> items |
The items, as buttons for actions and links for navigation. |
command="hide-popover" |
Closes the menu after an action is picked. |
Accessibility
- The items are native buttons and links, so they are announced with their real role and work with Enter and Space.
- The popover follows its trigger in the focus order, so pressing Tab after opening moves into the menu, and Escape closes it and returns focus to the trigger.
- Arrow key navigation between items, as in a desktop app menu, is not built in. It
needs a script, and if you add one, add
role="menu"androle="menuitem"with it, not before. - An icon only trigger needs an
aria-label, such as “More actions”.
Variants
Icon trigger
A compact trigger at the end of a row, as on a table row or a card. The menu opens
toward the inline start, so it stays on screen, by setting --menu-position on the
popover.
<button type="button" commandfor="row-menu" command="toggle-popover" aria-label="More actions">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
</button>
<menu id="row-menu" popover class="action-menu list-none" role="list" style="--menu-position: block-end span-inline-start">
<li><a href="#" class="btn w-full align" style="--align: center start">View details</a></li>
<li><a href="#" class="btn w-full align" style="--align: center start">Edit</a></li>
<li><button type="button" commandfor="row-menu" command="hide-popover" class="w-full align" style="--align: center start">Duplicate</button></li>
</menu>
<style>
.action-menu {
--btn-stroke: transparent;
min-inline-size: 12rem;
padding: 0.25rem;
border: 1px solid color-mix(in oklab, var(--root-bg) 75%, var(--root-fg));
border-radius: 0.5rem;
background-color: color-mix(in oklab, var(--root-bg) 88%, white);
color: var(--root-fg);
box-shadow: var(--shadow-2);
@supports (position-area: block-end) {
inset: auto;
margin: 0.25rem 0;
position-area: var(--menu-position, block-end span-inline-end);
position-try-fallbacks: flip-block, flip-inline;
}
}
</style>Related: Action List, Tooltip, Select.
FAQ
Does the menu need JavaScript?
Not to open, close, or position it. The Popover API handles opening, closing on Escape or an outside click, and the top layer, and CSS anchor positioning places it under the trigger. You only need JavaScript for what the items actually do, and for arrow key navigation if you want it.
Why not role="menu" and role="menuitem"?
Those roles promise application style keyboard handling, where arrow keys move between items and Tab leaves the menu. Without a script that does exactly that, the roles make the menu harder to use, not easier. Plain buttons and links in a list work with Tab and Enter and are announced correctly. Add the menu roles only together with the script that implements them.
How is the menu positioned under its button?
Opening a popover from an invoker button makes that button its implicit anchor, so position-area can place the menu against it with no anchor names. position-try-fallbacks flips it above or to the other side when there is no room. If you open the popover from script instead, give the button an anchor-name and the menu a matching position-anchor.
What happens in browsers without anchor positioning?
The positioning rules sit in an @supports block, so those browsers keep the default popover placement, centered in the viewport. The menu still opens, closes, and works, it is just not attached to the button.