Overlays and menus
Dropdown Menu
A Base UI menu with submenus, checkable items, groups, and keyboard navigation.
Install
npx shadcn@latest add https://4af.selfsimilar.dev/r/dropdown-menu.jsonUsage
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
<DropdownMenu>
<DropdownMenuTrigger>Actions</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem disabled disabledReason="Billing is available on the Pro plan.">
Billing
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>API
Main exports: root, trigger, content, item, checkbox, radio, submenu, shortcut, and nested-select parts
A Base UI menu with keyboard navigation, safe submenu pointer travel, keyboard shortcut hints, destructive rows, navigation submenus, and searchable single and multi-select subpanels.
DropdownMenu loader={<MySpinner />} supplies the menu's loading indicator, including portalled
rows. Omit it to inherit LoaderProvider,
with AsciiLoader as the fallback. DropdownMenuActionItem and DropdownMenuCheckboxItem accept
loader for one row; a custom icons.pending takes precedence.
A disabled row can take a disabledReason. Hovering the row then opens a tooltip on its right
explaining why, without the usual tooltip delay:
<DropdownMenuItem disabled disabledReason="Billing is available on the Pro plan.">
Billing
</DropdownMenuItem>Nested panels
Dropdown Menu includes three nested-panel forms:
- a navigation submenu;
- a single-select panel with the chosen value shown in the parent row and a checkmark in the panel;
- a multi-select panel using small Checkbox components and an
N selectedparent label.
Single- and multi-select panels can include in-panel search. Their parent highlight stays active while the pointer is on the parent, inside the nested panel, or moving through the safe pointer corridor.
Nested panels open immediately when their parent row is hovered; the safe pointer corridor still keeps them open while the pointer moves into the panel. The initially focused child row stays unhighlighted until it is hovered or reached through keyboard navigation.
Select-style triggers keep their label or placeholder on the left and show the selected value or selection count, dimmed, immediately before the chevron. Searchable panels use a plain text field without a leading search icon, and the no-results state is the same height as one option row.
A checkbox item can also read as a toggle button rather than a ticked row. Its children may be
a function of checked state, and indicator={false} drops the left tick and its indent so the
icon and label carry the state:
<DropdownMenuCheckboxItem indicator={false}>
{(checked) => (
<>
<Morph activeKey={checked ? "on" : "off"} items={{ on: <BellOff />, off: <Bell /> }} />
<Morph activeKey={checked ? "on" : "off"} items={{ on: "Unmute", off: "Mute" }} />
</>
)}
</DropdownMenuCheckboxItem>Use fillIcon instead when the glyph keeps its shape, such as a bookmark, star, or heart that
only fills in. Either way, the row keeps its menuitemcheckbox semantics.
Rows left-align every Morph inside them and mute a morphing leading icon to match a plain one.
Don't pass place-items-start or a text colour at the call site.
Asynchronous rows
DropdownMenuActionItem runs an action, shows the result, and reverts, like Action Button:
<DropdownMenuActionItem
icon={<Link2 />}
action={() => navigator.clipboard.writeText(url)}
labels={{ success: "Copied" }}
announce={{ success: "Link copied" }}
>
Copy link
</DropdownMenuActionItem>It defaults to closeOnClick={false} so the result stays visible. Override it when the result
shows on the page behind the menu instead. It takes the same labels, icons, revertAfter, pendingDelay, announce, onSuccess, and
onError as Action Button, and both morph between states without resizing the row.
For a row that should stay in its new state, give DropdownMenuCheckboxItem an action. It
keeps its current state while pending and calls onCheckedChange only after success, like
Action Toggle:
<DropdownMenuCheckboxItem action={(muted) => setMuted(id, muted)}>
Mute thread
</DropdownMenuCheckboxItem>Without action, it is a plain synchronous checkbox. With it, the tick stays and the loader runs
in its slot until the work settles; on failure the state is unchanged. With indicator={false}
there is no tick slot for the loader, so the children function receives
status as a second argument for the caller to render.
Both async rows announce their status from outside the row element, so the status text never becomes part of the row's accessible name.