Component Porchlight CSS

Popover menu

A dropdown menu anchored to its trigger - native Popover API, CSS anchor positioning, @starting-style entry/exit animation.

52 components 51 stable 1 experimental

Command

Search Porchlight

Popover menu

The .c-menu is a dropdown menu anchored to its trigger. It uses the native Popover API (top-layer rendering, light-dismiss, focus management) + CSS anchor positioning (tethered to the trigger) + @starting-style for a native enter/exit animation. No JavaScript required.

Semantic HTML

<div class="c-menu">
  <button class="c-button c-menu__trigger" popovertarget="account-menu">
    Account
  </button>
  <div class="c-menu__popover" id="account-menu" popover>
    <a href="#">Settings</a>
    <a href="#">Billing</a>
    <hr class="c-menu__divider" />
    <button type="button" data-tone="danger">Sign out</button>
  </div>
</div>

The trigger’s popovertarget="id" opens the popover declaratively - no JS. The popover element MUST have the popover attribute.

Rich Menu Rows

Plain direct-child links and buttons work. For denser enterprise menus, use the optional .c-menu__item* anatomy for icons, descriptions, shortcuts, and selected/current rows.

<div class="c-menu__popover" id="reports-menu" popover>
  <a class="c-menu__item" href="/reports" aria-current="page">
    <svg class="c-menu__item-icon" aria-hidden="true">...</svg>
    <span class="c-menu__item-body">
      <span class="c-menu__item-label">Overview</span>
      <span class="c-menu__item-description">Executive KPI rollup</span>
    </span>
    <kbd class="c-menu__item-shortcut">R</kbd>
  </a>
  <button class="c-menu__item" type="button" data-tone="danger">
    Delete report
  </button>
</div>

Rows can be real <a> or <button> elements. [aria-current], [aria-selected="true"], and [data-selected] get the selected/current treatment; [data-tone="danger"] gets destructive color.

Class contract

SelectorRole
.c-menuThe wrapper (inline-block).
.c-menu__triggerThe opening button (gets anchor-name).
.c-menu__popoverThe popover element.
.c-menu__itemOptional row class for links/buttons.
.c-menu__item-iconLeading icon slot.
.c-menu__item-bodyLabel + description stack.
.c-menu__item-labelTruncated primary row text.
.c-menu__item-descriptionSecondary row text.
.c-menu__item-shortcutTrailing keyboard shortcut or hint.
.c-menu__dividerA horizontal rule between item groups.
Items <a>, <button>Direct children of .c-menu__popover.
[data-tone="danger"]Destructive item (danger-text / danger-bg on hover).
[data-placement="inline-end"]Sidebar/flyout placement beside the trigger.

Multiple menus

Each menu needs a unique anchor name. Set --c-menu-anchor per instance:

<div class="c-menu" style="--c-menu-anchor: --menu-a;">...</div>
<div class="c-menu" style="--c-menu-anchor: --menu-b;">...</div>

The default --pl-menu-anchor works for a single menu on the page.

Use data-placement="inline-end" for desktop sidebar or icon-rail flyouts. This changes the anchor position from below the trigger to beside it.

<div
  class="c-menu"
  data-placement="inline-end"
  style="--c-menu-anchor: --sidebar-menu;"
>
  ...
</div>

Tokens consumed

--pl-color-{surface,surface-2,border,text,danger-bg,danger-text}, --pl-focus-color, --pl-shadow-3, --pl-radius-{lg,sm}, --pl-space-{1,2,3}, --pl-duration-2, --pl-ease-standard, --pl-overlay-popover-bg, and --pl-menu-row-*.

Anchor positioning

Anchor positioning is @supports-gated. Chrome 149+ tethers the popover to the trigger’s block-end span-inline-end corner via position-area. Browsers without anchor support fall back to the UA’s default centering. inset: auto resets the UA centering so the anchor positioning takes over.

Enter / exit animation

The popover animates with @starting-style (opacity + translateY) and transition-behavior: allow-discrete on overlay/display so the exit transition runs (normally display: none kills transitions instantly).

Accessibility

  • Keyboard: Tab to the trigger, Enter/Space to open. Esc closes and returns focus to the trigger (native Popover API behavior). Light-dismiss (click outside) also closes.
  • Focus management: the Popover API handles focus trap and restore natively - no JS needed.
  • Items: each item is a real <a> or <button> (full-width, keyboard-focusable, semantic role).
  • Forced colors: border falls back to ButtonBorder, focus to Highlight.