A menu on the native Popover API — the trigger opens a list of DropdownItems in the top layer, with sections and submenus.

Last updated

Uses JavaScript

Adds the menu keyboard pattern (ArrowDown on the trigger opens and focuses the first item; arrows, Home and End move; ArrowRight / ArrowLeft open and close submenus), closes the whole menu when an item is activated, opens submenus on hover, and dispatches apogee:dropdown-open / -close.

Without JS: The menu opens and closes natively (popovertarget), including Escape and outside click. Without JS there is no arrow-key navigation, items don't close the menu on activation, and submenus open on click only.

Show code
<Dropdown label="Options">
<DropdownItem href="#">Edit</DropdownItem>
<DropdownItem href="#">Duplicate</DropdownItem>
<DropdownItem href="#">Delete</DropdownItem>
</Dropdown>

Add

npx @ava-ui/apogee add dropdown

Uses @lucide/astro (ChevronDown, ChevronRight) and utils. Ships DropdownItem.astro, DropdownSection.astro and DropdownSub.astro in the same folder.

Usage

---
import Dropdown from '@/components/apogee/Dropdown/Dropdown.astro'
import DropdownItem from '@/components/apogee/Dropdown/DropdownItem.astro'
---
<Dropdown label="Options" placement="bottom-end">
  <DropdownItem href="/edit">Edit</DropdownItem>
  <DropdownItem color="critical">Delete</DropdownItem>
</Dropdown>

Anatomy

  • Trigger — a <button popovertarget> showing label and a chevron that rotates while open. Or your own element in the anchor slot: give it popovertarget (the menu id) and aria-haspopup="menu".
  • Menu — the role="menu" popover, anchored to the trigger; default slot. Fill it with DropdownItems (role="menuitem"), DropdownSections (role="group") and DropdownSubs (an item that opens a nested menu).

API

@composable — exposed to Content Editors in AVA. Any other attribute passes through to the root <div>.

Prop Type Default Description
label string — Trigger text and the menu’s accessible name.
id string auto-generated Menu id. Set it when an anchor element needs popovertarget.
placement 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'start' | 'start-top' | 'start-bottom' | 'end' | 'end-top' | 'end-bottom' 'bottom-start' Side and alignment; flips when out of room. See Popover for all twelve.
width 'trigger' | string — 'trigger' matches the trigger’s width; otherwise any CSS length.
maxHeight string — Caps the menu height; items scroll past it.
class string — Extra classes on the root <div>, merged last.

A link when href is set, otherwise a <button>. @composable. Slots: start (icon), default (label), end (shortcut or badge).

Prop Type Default Description
href string — Renders an <a> instead of a <button>.
color 'neutral' | 'primary' | 'critical' 'neutral' Text and hover tint.
size 'small' | 'medium' | 'large' 'medium' Padding, text and icon size.
selected boolean false The chosen item: aria-current="true" and a faded fill (aria-checked with a checkbox or radio indicator).
indicator 'none' | 'check' | 'checkbox' | 'radio' 'none' A leading selection mark. The column stays reserved when unselected so labels align; checkbox / radio switch the role to menuitemcheckbox / menuitemradio.
disabled boolean false Native disabled on a button; aria-disabled + tabindex="-1" on a link.
class string — Extra classes, merged last.

A role="group" with an optional label and a rule above it when it is not first.

Prop Type Default Description
label string — Group heading (aria-labelledby).

An item that opens a nested menu beside it. Slots: start (icon), default (items).

Prop Type Default Description
label string — Item text and the nested menu’s accessible name.
size 'small' | 'medium' | 'large' 'medium' Matches the items around it.
placement placement 'end-top' Where the nested menu opens.

Placement

Show code
<Dropdown label="bottom-start" placement="bottom-start">…</Dropdown>
<Dropdown label="bottom-end" placement="bottom-end">…</Dropdown>
<Dropdown label="top-start" placement="top-start">…</Dropdown>
<Dropdown label="end-top" placement="end-top">…</Dropdown>

Custom trigger

Show code
<Dropdown id="dd-actions" label="Actions">
<Button slot="anchor" popovertarget="dd-actions" aria-haspopup="menu" variant="outline" color="primary">Actions</Button>
<DropdownItem href="#">Edit</DropdownItem>
<DropdownItem href="#">Delete</DropdownItem>
</Dropdown>

Sizes

Show code
<DropdownItem size="small">…</DropdownItem>
<DropdownItem size="medium">…</DropdownItem>
<DropdownItem size="large">…</DropdownItem>

Colours

Show code
<DropdownItem color="neutral">Rename</DropdownItem>
<DropdownItem color="primary">Make default</DropdownItem>
<DropdownItem color="critical">Delete</DropdownItem>

Selected and disabled

Show code
<DropdownItem selected>Grid</DropdownItem>
<DropdownItem>List</DropdownItem>
<DropdownItem disabled>Board (soon)</DropdownItem>
<DropdownItem href="#" disabled>Timeline (soon)</DropdownItem>

Indicators

Every indicator:

Show code
<Dropdown label="View">
<DropdownItem indicator="check" selected>Comfortable</DropdownItem>
<DropdownItem indicator="check">Compact</DropdownItem>
<DropdownSection label="Show">
  <DropdownItem indicator="checkbox" selected>Line numbers</DropdownItem>
  <DropdownItem indicator="checkbox">Minimap</DropdownItem>
</DropdownSection>
<DropdownSection label="Wrap">
  <DropdownItem indicator="radio" selected>Soft</DropdownItem>
  <DropdownItem indicator="radio">Off</DropdownItem>
</DropdownSection>
</Dropdown>

The mark reflects the server-rendered selected; the item is a plain button or link, so your own click handler updates state and re-renders, as with any menu.

Sections and end slot

Show code
<Dropdown label="File">
<DropdownSection label="Edit">
  <DropdownItem><Pencil slot="start" /> Rename <Kbd slot="end">F2</Kbd></DropdownItem>
  <DropdownItem><Copy slot="start" /> Duplicate <Kbd slot="end">⌘D</Kbd></DropdownItem>
</DropdownSection>
<DropdownSection label="Danger">
  <DropdownItem color="critical"><Trash2 slot="start" /> Delete <Kbd slot="end">⌫</Kbd></DropdownItem>
</DropdownSection>
</Dropdown>

Hover, click or ArrowRight opens the nested menu; ArrowLeft or Escape closes it. Picking an item closes the whole chain.

Show code
<Dropdown label="Options">
<DropdownItem><Pencil slot="start" /> Edit</DropdownItem>
<DropdownSub label="Share">
  <Share2 slot="start" />
  <DropdownItem><Link slot="start" /> Copy link</DropdownItem>
  <DropdownItem><Mail slot="start" /> Email</DropdownItem>
</DropdownSub>
<DropdownItem color="critical"><Trash2 slot="start" /> Delete</DropdownItem>
</Dropdown>

Width and max height

Show code
<Dropdown label="Pick a month (matches the trigger)" width="trigger" maxHeight="10rem">
…twelve items…
</Dropdown>

States

  • Closed / open — the native popover toggle; the chevron rotates 180° while open.
  • Out of room — the menu flips to the opposite side, no JS.
  • Hover / focus — the trigger tints on hover; keyboard focus shows a visible ring. Items tint on hover and on keyboard focus.
  • No JS — see the JsNote above.

Events

The root <div> dispatches apogee:dropdown-open and apogee:dropdown-close (bubbling) alongside the native toggle event on the menu. Activating an item closes the menu, nested menus included.

Examples

A user menu: a custom trigger, sections, a shortcut in the end slot and a critical last item:

Show code
<Dropdown id="dd-user" label="Account" placement="bottom-end">
<Button slot="anchor" popovertarget="dd-user" aria-haspopup="menu" variant="ghost" color="neutral">Ada Lovelace</Button>
<DropdownSection label="ada@example.com">
  <DropdownItem href="#"><User slot="start" /> Profile</DropdownItem>
  <DropdownItem href="#"><Settings slot="start" /> Settings <Kbd slot="end">⌘,</Kbd></DropdownItem>
</DropdownSection>
<DropdownSection>
  <DropdownItem color="critical"><LogOut slot="start" /> Sign out</DropdownItem>
</DropdownSection>
</Dropdown>

Accessibility

Key Action
Enter Space On the trigger, toggle the menu; on an item, activate it and close the menu.
↓ On the closed trigger, opens the menu and focuses the first item.
↓ ↑ Move between items, wrapping at the ends.
Home End First / last item.
→ On a submenu item, opens the submenu and focuses its first item (← under dir="rtl").
← Inside a submenu, closes it and returns focus to its item (→ under dir="rtl").
Esc Closes the open menu or submenu and returns focus to its trigger.
  • The menu has role="menu" and takes label as its accessible name; DropdownItems carry role="menuitem"; sections are role="group".
  • The trigger is a <button popovertarget> with aria-haspopup="menu"; the browser sets aria-expanded. Clicking outside closes the menu (native).
  • A disabled link item keeps its text but is aria-disabled and out of the tab order.
  • No JS — the menu still opens, closes and light-dismisses; items are reachable with Tab in document order.

Tokens

Token Used for
--border-neutral Trigger + menu border, section rules
--background-page Trigger background
--background-neutral-faded-highlighted Trigger and neutral item hover
--background-neutral-faded Selected neutral item
--background-primary-faded / --background-critical-faded Primary / critical item hover and selected
--foreground-neutral / -primary / -critical Item text by colour
--foreground-neutral-faded Chevrons, icons, end slot, section labels
--foreground-disabled Disabled items
--background-elevation-overlay Menu background
--outline-color-focus Trigger focus ring
--duration-fast / --ease-standard Menu open fade/scale

Do / Don’t

  • Do keep the trigger label short — it’s the only cue for what the menu holds.
  • Do order items by frequency of use, with destructive actions last, in their own section and color="critical".
  • Don’t pack more than a handful of items in one menu — use a full page or Combobox / CommandPalette for long lists.
  • Don’t nest more than one level of DropdownSub.

Content

  • Item labels are short verbs or verb phrases (“Edit”, “Duplicate”, “Delete”), sentence case, no ending punctuation.
  • The trigger label names what the menu contains, not a bare “Menu” or “Options” when a more specific noun fits.