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 role="list" style="--btn-stroke: transparent; --popover-padding: 0.25rem; --popover-offset: 0.25rem 0; --size-x: 12rem">
    	<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>

    Usage

    1. A trigger <button> with commandfor and command="toggle-popover" opens and closes the menu. The browser also exposes its expanded state to assistive technology.
    2. The menu is a <menu popover role="list">, which removes the list styles. <menu> is a list of commands, so the items sit directly in it as <li> elements and the popover needs no extra wrapper. popover (the auto state) gives light dismiss, so Escape or a click outside closes it, and opening one menu closes any other.
    3. Each <li> holds a button or link. Group items with an <li> that holds an <hr>. A command="hide-popover" on an item closes the menu once the action is picked. Links close it by navigating.
    4. Fylgja Base draws the panel and anchors it under the trigger, so the menu needs no custom CSS. Set --popover-position to any position-area value to change where it opens, such as block-end span-inline-start for a trigger at the end of a row. --popover-padding and --popover-offset tune the panel, and --btn-stroke removes the button borders.
    5. A .toggle-rotate icon in the trigger flips while the menu is open, as long as the popover comes right after the trigger.

    Anatomy

    Part Responsibility
    Trigger <button command> Toggles the menu and acts as its anchor.
    <menu popover role="list"> 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" and role="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 --popover-position on the popover.

  • View details
  • Edit
  • <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 role="list" style="--btn-stroke: transparent; --popover-padding: 0.25rem; --popover-offset: 0.25rem 0; --size-x: 12rem; --popover-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>

    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 in Fylgja Base 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 the position-area in Fylgja Base places the menu against it with no anchor names, and 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.

    Does this work with older versions of Fylgja?

    The menu needs no custom CSS from Fylgja Base 2.2.0 and Fylgja Utilities 2.2.0. With an older Base, add list-none to the menu and write your own rule for the panel, with inset auto, a margin, position-area, a background, and a shadow. With older Utilities, the toggle-rotate class does not react to a popover, so add a rule that rotates the icon while the next element matches popover-open.

    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.

    Should I use popovertarget or commandfor?

    Both work. The examples use commandfor with command="toggle-popover" to match the invoker commands used for the Modal and Offcanvas. popovertarget has been around longer, so reach for it if you need to support older browsers.