Dropdown Menu
role="menu" on a popover="auto" element. Outside-click and Escape dismissal come free from the popover. Arrow-key roving between items is the one part of the WAI-ARIA menu pattern that has to be wired up by hand, there's no native menu element yet. A morph variant further down grows the panel out of its own trigger instead of fading in place.
Usage
import { Button, DropdownMenu, MenuChevron, MenuItem, MenuSeparator } from "@kernelui-lib/react";
function ActionsMenu() {
return (
<DropdownMenu
align="end"
offset={4}
className="my-menu"
render={<Button variant="secondary" iconEnd={<MenuChevron />}>Actions</Button>}
>
<MenuItem onSelect={() => {}}>Edit</MenuItem>
<MenuItem render={<a href="/docs" />}>Documentation</MenuItem>
<MenuSeparator />
<MenuItem destructive onSelect={() => {}}>
Delete
</MenuItem>
</DropdownMenu>
);
}Playground
Toggle every adjustable prop and watch the component — and the code — update live.
Same component — Kernel look stripped. See Platforms.
Usage
<DropdownMenu render={<Button variant="secondary" iconEnd={<MenuChevron />}>Actions</Button>}>
<MenuItem onSelect={() => {}}>Edit</MenuItem>
<MenuItem onSelect={() => {}}>Duplicate</MenuItem>
<MenuSeparator />
<MenuItem destructive onSelect={() => {}}>
Delete
</MenuItem>
</DropdownMenu><kernel-dropdown-menu>
<kernel-button slot="trigger" variant="secondary">
Actions
<svg class="kernel-menu-chevron" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" /></svg>
</kernel-button>
<kernel-menu-item>Edit</kernel-menu-item>
<kernel-menu-item>Duplicate</kernel-menu-item>
<kernel-menu-separator></kernel-menu-separator>
<kernel-menu-item destructive>Delete</kernel-menu-item>
</kernel-dropdown-menu>Props
| Component | Prop | Type |
|---|---|---|
DropdownMenu | presentation | "popover" (default) | "disclosure" |
DropdownMenu | render | the trigger (required) |
DropdownMenu | renderContent | RenderProp — replace the popup |
DropdownMenu | className | string | (state) => string |
DropdownMenu | placement | "top" | "bottom" | "left" | "right" |
DropdownMenu | align | "start" | "center" | "end" |
DropdownMenu | offset | number (px) |
MenuItem | render | RenderProp — e.g. <a> / router Link |
MenuItem | className | string | (state) => string |
MenuItem | onSelect | () => void |
MenuItem | destructive | boolean |
MenuChevron | — | no props — drop into a trigger's iconEnd; rotates via aria-expanded |
DropdownMenuMorph | render | the trigger (required) — same shape as DropdownMenu's |
DropdownMenuMorph | renderContent | RenderProp — replace the popup |
DropdownMenuMorph | className | string | (state) => string |
DropdownMenuMorph | placement | "top" | "bottom" | "left" | "right" |
DropdownMenuMorph | align | "start" | "center" | "end" |
DropdownMenuMorph | offset | number (px) |
Native disclosure presentation
Set presentation="disclosure" for a menu anchored within its parent, such as navigation in a fixed mobile header. This renders a real details with a summary trigger. HTML handles toggling; CSS handles repeated entry and exit through::details-content. It creates no top-layer backdrop and does not change page scrolling. Escape, outside clicks, Tab leaving the menu, and item selection close it; arrow keys move between items.
Render a summary, not a button inside a summary. The disclosure stays in its ancestor's stacking context and can be clipped by ancestor overflow. It does not flip to avoid viewport edges; use the default popover presentation when those positioning features are needed. Browsers without ::details-content transitions still toggle natively, with immediate closing. Reduced motion is immediate.
Navigation
Usage
<DropdownMenu
presentation="disclosure"
align="end"
render={<Button render={<summary />}>Menu</Button>}
>
<MenuItem render={<a href="/work" />}>Work</MenuItem>
<MenuItem render={<a href="/about" />}>About</MenuItem>
</DropdownMenu>Usage
<kernel-dropdown-menu presentation="disclosure" align="end">
<summary slot="trigger">Menu</summary>
<kernel-menu-item href="/work">Work</kernel-menu-item>
<kernel-menu-item href="/about">About</kernel-menu-item>
</kernel-dropdown-menu>Morph variant
DropdownMenuMorph is the same menu — it sharesMenuItem/MenuSeparator directly — with a different opening move: the panel's own box grows out of the trigger's exact footprint (width, height, and corner radius all animate) instead of scaling and fading in place.
That one visual difference forces a mechanical one: a popover's hide can't be deferred, so there's no way to let the shrink-back- to-the-trigger animation finish before the panel actually disappears while staying popover="auto". This variant is popover="manual" instead, with its own outside- click and Escape dismissal wired up by hand — the same tradeoffContextMenu and Combobox already make for their own manual popovers. Selecting an item still plays the full shrink animation before the menu closes.
The trigger's own icon rotation/cross-fade (the + that becomes a × below) is authored in the demo, not baked into the component — same convention as MenuChevron: what renders is always visible at the call site.
Usage
import { DropdownMenuMorph, MenuItem, MenuSeparator } from "@kernelui-lib/react";
<DropdownMenuMorph render={<PlusTrigger />}>
<MenuItem onSelect={() => {}}>New file</MenuItem>
<MenuItem onSelect={() => {}}>New folder</MenuItem>
<MenuSeparator />
<MenuItem onSelect={() => {}}>Upload</MenuItem>
</DropdownMenuMorph>Accessibility
Opening moves focus to the first item. Arrow Up/Down, Home, and End move between items. Selecting an item, or pressing Escape, closes the menu. Link items keep native navigation, modifier-clicks, and new-tab behaviour — selection closes the menu withoutpreventDefault.