Dropdown Menu
Dropdown menus display a list of actions or options that a user can choose from, triggered by a button. Built on Base UI Menu with the same parts shape as shadcn's dropdown-menu — every item is a horizontal row with an optional left slot, label, and optional right slot.
For composite picker patterns (model selector, datasource selector, settings toggles), see Selectors.
Examples
Account menu (icons + radio submenu)
Row actions (submenus, destructive)
Items with tooltips (tooltip prop)
Single select (right-slot checkmark)
Multi select (left-slot checkbox)
Searchable, single select
Searchable, multi select
Radio items
Groups with full-width dividers
Anatomy
Every row has the same horizontal slot layout: an optional left slot, the label, and an optional right slot.
| Variant | Left slot | Right slot |
|---|---|---|
DropdownMenuItem | prefix (icon) | suffix / shortcut / selected checkmark |
DropdownMenuCheckboxItem | Checkbox (auto) | Optional checkmark when checked (off by default in pickers) |
DropdownMenuRadioItem | — | Checkmark when selected |
DropdownMenuSubTrigger | — | Chevron (auto) |
Separators bleed edge-to-edge of the popup (-mx-1), so use them to split groups visually.
The trigger receives a data-popup-open attribute while the menu is open — wire your own active styles to it (the demos pass className="data-[popup-open]:bg-tertiary" through Button asChild, matching the outline variant's hover background) so users can tell which trigger owns the visible popover.
Usage
Basic with shortcuts
<DropdownMenu>
<DropdownMenuTrigger>
<button>Open</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Single select (right-slot checkmark)
Render plain DropdownMenuItems and pass selected to draw a checkmark on the right. The menu closes on select by default.
const [value, setValue] = useState("Option 1");
<DropdownMenuContent>
<DropdownMenuLabel>Group title</DropdownMenuLabel>
{options.map((option) => (
<DropdownMenuItem
key={option}
selected={value === option}
onSelect={() => setValue(option)}
>
{option}
</DropdownMenuItem>
))}
</DropdownMenuContent>;Multi select (left-slot checkbox)
DropdownMenuCheckboxItem renders a checkbox visual on the left. Pass closeOnClick={false} so the menu stays open while toggling, and showSelectedIndicator={false} so checked rows don't double up with a right-slot checkmark — the left checkbox already communicates state.
<DropdownMenuContent>
<DropdownMenuLabel>Labels</DropdownMenuLabel>
{labels.map((label) => (
<DropdownMenuCheckboxItem
key={label}
checked={state[label]}
onCheckedChange={(checked) => setState((p) => ({ ...p, [label]: checked === true }))}
closeOnClick={false}
showSelectedIndicator={false}
>
{label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>The prop pairing is intentional: single-select uses the right-slot checkmark via DropdownMenuItem selected, multi-select uses the left-slot checkbox via DropdownMenuCheckboxItem — never both. If you really need extra emphasis on the checked row, set showSelectedIndicator to true to bring the right-slot checkmark back.
Searchable (Linear style)
Drop a DropdownMenuSearch as the first child of DropdownMenuContent. It autofocuses on open and renders a full-width divider below — the input is just a plain text input by default. Pass prefix / suffix to add an icon, clear button, or hint. Filtering logic is yours; the demos use Fuse.js with threshold: 0.4 and a memoized index, which tolerates typos and partial matches without re-allocating on each keystroke.
import { IconMagnifyingGlass } from "@rogo-technologies/ui/icons";
<DropdownMenuSearch
value={query}
onChange={(e) => setQuery(e.target.value)}
prefix={<IconMagnifyingGlass className="text-tertiary size-4" />}
/>;const [query, setQuery] = useState("");
const filtered = options.filter((o) => o.toLowerCase().includes(query.toLowerCase()));
<DropdownMenuContent className="min-w-72">
<DropdownMenuSearch
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search metrics..."
/>
{filtered.length === 0 ? (
<div className="text-tertiary px-2 py-6 text-center text-sm">No results</div>
) : (
filtered.map((option) => (
<DropdownMenuItem key={option} selected={value === option} onSelect={() => setValue(option)}>
{option}
</DropdownMenuItem>
))
)}
</DropdownMenuContent>;Arrow keys move focus through items even while the input has focus. Pass autoFocusOnMount={false} if you don't want the input to grab focus on open.
Radio items
Single-select via a DropdownMenuRadioGroup. Selected items show a checkmark in the right slot.
<DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
<DropdownMenuRadioItem value="relevance">Relevance</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="date">Date</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name">Name</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>Tooltips on items (tooltip prop)
Pass tooltip on any DropdownMenuItem to explain what the row does on hover/focus — no need to wrap anything yourself. DropdownMenuContent already provides a TooltipProvider internally, so the first tooltip waits ~400ms, and once it opens, hovering neighbours shows their tooltips instantly.
<DropdownMenuItem
prefix={<IconEyeOpen className="text-tertiary" />}
tooltip="Can open and read but cannot make changes."
>
Viewer
</DropdownMenuItem>tooltip accepts a string or any ReactNode, so you can pass richer content when needed:
<DropdownMenuItem
prefix={<IconSettingsGear2 className="text-tertiary" />}
tooltip={
<div className="flex flex-col gap-1">
<span className="font-medium">Admin</span>
<span className="text-xs opacity-80">
Full control — share, manage members, change settings, delete.
</span>
</div>
}
>
Admin
</DropdownMenuItem>By default the tooltip appears on the right so it clears the menu. Override with tooltipSide="top" | "right" | "bottom" | "left" if the menu sits against the viewport edge and the right side gets clipped — Base UI's collision detection will also flip automatically if needed.
Arbitrary content in prefix / suffix
prefix and suffix on DropdownMenuItem are typed React.ReactNode, so any element works — toggles, badges, avatars, kbd hints, count pills. Pair with closeOnClick={false} so toggling the control doesn't dismiss the menu, and stop event propagation inside the control if its click shouldn't also fire onSelect on the row.
<DropdownMenuItem
closeOnClick={false}
onSelect={() => setEnabled(!enabled)}
prefix={<IconBell className="text-tertiary" />}
suffix={<Switch checked={enabled} onCheckedChange={setEnabled} label="Push notifications" />}
>
Push notifications
</DropdownMenuItem>DropdownMenuCheckboxItem and DropdownMenuRadioItem manage their own slots — for non-default controls (toggles instead of checkboxes, custom selected indicators, etc.) use a plain DropdownMenuItem and put your control in suffix.
Items with a description (two-line layout)
When the option list is small and the differences between items matter (model pickers, plan selectors, integration choices), put the description inline instead of behind a tooltip. Pass the provider/leading icon via prefix and a vertical flex column as children — the row stays as one DropdownMenuItem so selected still drives the right-slot checkmark.
<DropdownMenuItem
selected={value === model.id}
onSelect={() => setValue(model.id)}
prefix={<ProviderIcon />}
>
<div className="flex min-w-0 flex-col leading-tight">
<span className="text-primary truncate">{model.name}</span>
<span className="text-tertiary truncate text-xs">{model.description}</span>
</div>
</DropdownMenuItem>leading-tight pulls the two lines closer than the row's default; min-w-0 + truncate on each line stops a long description from blowing out the menu width.
Submenus
Submenus open on hover and on keyboard highlight (after a 100 ms delay) so users can peek inside without committing — focus only moves into the submenu when the user presses Right Arrow or Enter. Pass openOnHover={false} on the sub-trigger to disable hover-open for nested cases where the eager reveal feels noisy.
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<IconShareOs className="text-tertiary" />
<span>Share</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>
<IconChainLink1 className="text-tertiary" />
<span>Copy link</span>
</DropdownMenuItem>
<DropdownMenuItem>
<IconEmail1 className="text-tertiary" />
<span>Email</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>Destructive items
<DropdownMenuItem actionType="destructive">Delete</DropdownMenuItem>Trigger with asChild
Compose the trigger with a real Button. The data-popup-open attribute is forwarded to the rendered element, so you can darken the trigger while the menu is open by passing a Tailwind selector through className:
import { Button } from "@rogo-technologies/ui/button";
import { IconChevronDownSmall } from "@rogo-technologies/ui/icons";
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
suffix={<IconChevronDownSmall />}
className="data-[popup-open]:bg-tertiary"
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>;Positioning
<DropdownMenuContent side="right" align="start" sideOffset={8}>…</DropdownMenuContent>
<DropdownMenuContent side="top" align="center">…</DropdownMenuContent>Accessibility
- Menu uses
role="menu"withrole="menuitem"children - Keyboard:
↑/↓to move,Enter/Spaceto select,Escto close - Submenus open with
→and close with← - Focus is trapped while open
- Closes when clicking outside or pressing
Escape - Checkbox items use
role="menuitemcheckbox" - Radio items use
role="menuitemradio"withinrole="group" DropdownMenuSearchis a plain text input — arrow keys still navigate items so keyboard users can search and select without leaving the keyboard
API
DropdownMenu
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial open state for uncontrolled usage. |
onOpenChange | (open: boolean) => void | — | Callback when open state changes. |
DropdownMenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as the child element instead of a button. |
DropdownMenuContent
Convenience wrapper that renders Portal → Positioner → Popup.
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "bottom" | Side of the trigger to anchor. |
sideOffset | number | 4 | Offset from the trigger edge in px. |
align | "start" | "center" | "end" | "start" | Alignment along the side axis. |
alignOffset | number | — | Offset from the alignment edge. |
collisionPadding | number | Padding | — | Padding from viewport edges for collision detection. |
positionerClassName | string | — | Additional class for the positioner wrapper. |
DropdownMenuSearch
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled input value. |
onChange | handler | — | Fired when the user types. |
placeholder | string | "Search options…" | Placeholder text. |
autoFocusOnMount | boolean | true | Focuses the input as soon as the popup mounts. |
prefix | ReactNode | — | Slot rendered before the input (e.g. a search icon). |
suffix | ReactNode | — | Slot rendered after the input (e.g. a clear button). |
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | Draws a checkmark in the right slot. Use for single-select rows. |
prefix | ReactNode | — | Left-slot content (icon, badge, custom checkbox). |
suffix | ReactNode | — | Right-slot content. Overrides the selected checkmark. |
inset | boolean | false | Adds left padding to align with items that have icons. |
actionType | "default" | "destructive" | "default" | Visual style — destructive shows red text. |
onSelect | (event: MouseEvent) => void | — | Callback when selected (mapped to onClick). |
tooltip | ReactNode | — | Shown on hover/focus. DropdownMenuContent wraps items in a shared TooltipProvider, so neighbours show instantly after the first opens. |
tooltipSide | "top" | "right" | "bottom" | "left" | "right" | Side the tooltip appears on. |
disabled | boolean | false | Disables the item. |
DropdownMenuCheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | — | Controlled checked state. |
onCheckedChange | (checked: boolean) => void | — | Callback when checked state changes. |
closeOnClick | boolean | true | Set to false to keep the menu open while toggling multiple options. |
showSelectedIndicator | boolean | true | Right-slot confirming checkmark. Set to false for left-checkbox only — the canonical multi-select pattern. |
DropdownMenuRadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled selected value. |
onValueChange | (value: string) => void | — | Callback when selection changes. |
DropdownMenuRadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique value identifying this item. |
DropdownMenuSubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Adds left padding for alignment. |
openOnHover | boolean | true | Opens the submenu when the trigger is hovered or keyboard-highlighted. Focus stays on the parent trigger until the user presses Right Arrow / Enter. |
delay | number | 100 | Hover delay in ms before the submenu opens. |
DropdownMenuSubContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "right" | Side relative to the sub-trigger. |
sideOffset | number | -2 | Offset from the sub-trigger edge. |
align | "start" | "center" | "end" | "start" | Alignment along the side axis. |
DropdownMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Adds left padding for alignment. |
Other subcomponents
| Component | Purpose |
|---|---|
DropdownMenuGroup | Wraps a related set of items. |
DropdownMenuSeparator | Full-width divider between groups of items. |
DropdownMenuShortcut | Right-aligned keyboard-shortcut hint inside a menu item. |
Guidelines
Do
- Use dropdown menus for contextual actions related to a specific element
- Group related items with labels and separators
- Place destructive actions last, separated visually
- Keep menu items concise — use short, action-oriented labels
- Use
selectedonDropdownMenuItemfor single-select pickers (filters with one value) - Use
DropdownMenuCheckboxItemfor multi-select pickers (filters with many values) - Use
DropdownMenuRadioGroupwhen the values are a fixed enum (sort order, view mode) - Add a
DropdownMenuSearchwhen the list could exceed ~10 items - Add a left-side icon to every item in a settings or row-actions menu — keep them in
text-tertiaryso they don't compete with the label (destructive items skiptext-tertiaryso the icon takes the destructive color) - Style your trigger's open state via the
data-popup-openattribute so users can see which trigger owns the visible popover
Don't
- Don't use dropdown menus for navigation — use a navigation component instead
- Don't nest submenus more than one level deep — it becomes difficult to use
- Don't place too many items in a single menu without a search input
- Don't use dropdown menus for form inputs — use select, combobox, or radio groups instead
- Don't put both a
selectedcheckmark and a customsuffixon the sameDropdownMenuItem—suffixwins