Dropdown
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 codeHide 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>showinglabeland a chevron that rotates while open. Or your own element in theanchorslot: give itpopovertarget(the menuid) andaria-haspopup="menu". - Menu — the
role="menu"popover, anchored to the trigger; default slot. Fill it withDropdownItems (role="menuitem"),DropdownSections (role="group") andDropdownSubs (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. |
DropdownItem props
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. |
DropdownSection props
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). |
DropdownSub props
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 codeHide 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 codeHide 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 codeHide code
<DropdownItem size="small">…</DropdownItem>
<DropdownItem size="medium">…</DropdownItem>
<DropdownItem size="large">…</DropdownItem>Colours
Show codeHide code
<DropdownItem color="neutral">Rename</DropdownItem>
<DropdownItem color="primary">Make default</DropdownItem>
<DropdownItem color="critical">Delete</DropdownItem>Selected and disabled
Show codeHide code
<DropdownItem selected>Grid</DropdownItem>
<DropdownItem>List</DropdownItem>
<DropdownItem disabled>Board (soon)</DropdownItem>
<DropdownItem href="#" disabled>Timeline (soon)</DropdownItem>Indicators
Every indicator:
Show codeHide 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 codeHide 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>Submenu
Hover, click or ArrowRight opens the nested menu; ArrowLeft or Escape
closes it. Picking an item closes the whole chain.
Show codeHide 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 codeHide 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 codeHide 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 takeslabelas its accessible name;DropdownItems carryrole="menuitem"; sections arerole="group". - The trigger is a
<button popovertarget>witharia-haspopup="menu"; the browser setsaria-expanded. Clicking outside closes the menu (native). - A disabled link item keeps its text but is
aria-disabledand out of the tab order. - No JS — the menu still opens, closes and light-dismisses; items are
reachable with
Tabin 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
labelshort — 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/CommandPalettefor 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
labelnames what the menu contains, not a bare “Menu” or “Options” when a more specific noun fits.