Skip to content

Dropdown Menu

Action menu using the native HTML Popover API. Click the button to open it. Click outside or press Escape to dismiss.

Wrap the trigger and menu in .af-dropdown. That names the button as a CSS anchor so the open menu sits underneath — popovertarget must still match the menu id. The empty popover attribute is popover="auto": one open at a time, light-dismissed.

<div class="af-dropdown">
  <button type="button" class="af-btn" popovertarget="dropdown-menu" aria-haspopup="menu">Menu</button>
  <ul id="dropdown-menu" popover class="af-dropdown-menu" role="menu">
    <li>
      <a href="#" role="menuitem">Item 1</a>
    </li>
    <li>
      <a href="#" role="menuitem">Item 2</a>
    </li>
    <li>
      <button type="button" role="menuitem">Item 3</button>
    </li>
  </ul>
</div>

Default is inline-start (left in LTR). Add af-is-end on .af-dropdown to align the menu’s end edge to the button’s end edge — typical for an account menu in a header. af-is-start is the explicit form of the default.

<div class="af-spread">
  <div class="af-dropdown">
    <button type="button" class="af-btn" popovertarget="dropdown-menu" aria-haspopup="menu">Menu</button>
    <ul id="dropdown-menu" popover class="af-dropdown-menu" role="menu">
      <li>
        <a href="#" role="menuitem">Item 1</a>
      </li>
      <li>
        <a href="#" role="menuitem">Item 2</a>
      </li>
    </ul>
  </div>
  <div class="af-dropdown af-is-end">
    <button type="button" class="af-btn" popovertarget="dropdown-menu-end" aria-haspopup="menu">Account</button>
    <ul id="dropdown-menu-end" popover class="af-dropdown-menu" role="menu">
      <li>
        <a href="#" role="menuitem">Profile</a>
      </li>
      <li>
        <a href="#" role="menuitem">Settings</a>
      </li>
      <li>
        <button type="button" role="menuitem">Sign out</button>
      </li>
    </ul>
  </div>
</div>

Put the modifier on .af-dropdown, or on .af-dropdown-menu if you are not using the wrapper.

  • .af-dropdown — wraps trigger + menu; scopes the CSS anchor so two dropdowns on a page don’t collide
  • .af-dropdown.af-is-start — align menu to the trigger’s inline-start edge (default)
  • .af-dropdown.af-is-end — align menu to the trigger’s inline-end edge
  • .af-dropdown-menu — menu container
  • .af-dropdown-menu-open — show the menu when you are not using popover (alternative to removing hidden)

Toggle [hidden] or .af-dropdown-menu-open yourself, and keep the trigger in a position: relative parent. Supporting browsers already do that work with popover.

<div style="position: relative;">
  <button type="button" class="af-btn" aria-haspopup="menu" aria-expanded="false">Menu</button>
  <ul class="af-dropdown-menu" role="menu" hidden>
    <li>
      <a href="#" role="menuitem">Item 1</a>
    </li>
  </ul>
</div>
  • Use a native <button> trigger and a <ul> menu. role="menu" / role="menuitem" are required — there is no native menu element.
  • popovertarget sets aria-expanded on the invoker. Do not set it yourself on the popover path.
  • Popover handles open/close, click-outside, and Escape. Arrow keys, Home/End, and typeahead stay in your JavaScript or kit.

Toggle works wherever Popover does. Sitting the menu under the button is CSS anchor positioning — progressive enhancement, same as the styled Select picker.

Feature ChromeEdgeSafariFirefox
popover Toggle, light-dismiss, and Escape with no JavaScript 114 Supported from 114 114 Supported from 114 17 Supported from 17 125 Supported from 125
anchor() Sits the open menu under the button (flips above if needed) 125 Supported from 125 125 Supported from 125 26 Supported from 26 147 Supported from 147

Safari versions cover iOS Safari too. Without support: the button does nothing if Popover is missing. If Popover works but anchor positioning does not, the menu still opens — centered on the viewport, from the user-agent stylesheet. Versions checked against MDN browser-compat-data, August 2026. See Browser Support for Airframe's baseline.