Dropdown Menu
A menu of actions or options triggered by a button
View MarkdownOverview
A dropdown menu displays a list of actions or options in a floating panel anchored to a trigger. Use it for contextual actions, account menus, view options, and settings. It supports labels, separators, checkboxes, radio groups, keyboard shortcuts, grouping, nested submenus, and optional in-place search, and is fully keyboard navigable.
Usage
Import the components:
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@eqtylab/equality";
Basic usage:
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="tertiary">
Open Menu
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem variant="danger">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Variants
With Separators and Labels
Use DropdownMenuLabel to title a section and DropdownMenuSeparator to divide groups of items. Items accept icons as children alongside a <span> label.
<DropdownMenuContent align="start">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User />
<span>Profile</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings />
<span>Settings</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
<span>Logout</span>
</DropdownMenuItem>
</DropdownMenuContent>
With Checkboxes
Use DropdownMenuCheckboxItem for options that toggle on and off independently. Control each item with checked and onCheckedChange.
const [showStatusBar, setShowStatusBar] = useState(true);
<DropdownMenuCheckboxItem
checked={showStatusBar}
onCheckedChange={setShowStatusBar}
>
Status Bar
</DropdownMenuCheckboxItem>;
With Radio Items
Use DropdownMenuRadioGroup with DropdownMenuRadioItem to select a single option from a set. The group is controlled with value and onValueChange.
const [position, setPosition] = useState("bottom");
<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>;
With Shortcuts
Use DropdownMenuShortcut to display a keyboard shortcut hint aligned to the end of an item.
<DropdownMenuItem>
<span>New Tab</span>
<DropdownMenuShortcut>⌘T</DropdownMenuShortcut>
</DropdownMenuItem>
With Submenu
Use DropdownMenuSub, DropdownMenuSubTrigger, and DropdownMenuSubContent to nest a menu inside an item. The submenu opens on hover or keyboard focus. Submenus can be nested to any depth — place another DropdownMenuSub inside a DropdownMenuSubContent to create a further level.
<DropdownMenuContent align="start">
<DropdownMenuItem>Back</DropdownMenuItem>
<DropdownMenuItem>Forward</DropdownMenuItem>
<DropdownMenuItem>Reload</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>More Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Save Page As...</DropdownMenuItem>
<DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
<DropdownMenuItem>Name Window...</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Developer Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Console</DropdownMenuItem>
<DropdownMenuItem>Network</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Profiling</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Performance</DropdownMenuItem>
<DropdownMenuItem>Memory</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuContent>
With Groups
Wrap related items in DropdownMenuGroup to associate a label with its items for assistive technology.
<DropdownMenuGroup>
<DropdownMenuLabel>File</DropdownMenuLabel>
<DropdownMenuItem>
<span>New File</span>
<DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Open File</span>
<DropdownMenuShortcut>⌘O</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
Search & filtering
Add a DropdownMenuSearch inside DropdownMenuContent to filter items in place. It is opt-in per menu — without it, the menu behaves exactly as before and the built-in typeahead still works. Items hide themselves when they don’t match, and matching is done against each item’s textValue, falling back to its rendered text.
Generally using DropdownMenuSearch over the default typeahead is encouraged for most dropdown menus in our apps.
Import the additional parts:
import { DropdownMenuSearch, DropdownMenuEmpty } from "@eqtylab/equality";
Reveal on typing
By default the search box is hidden and reveals as soon as you start typing — the first keystroke seeds the query. Add a DropdownMenuEmpty to show a “no results” row when nothing matches. Give items that lead with an icon or avatar a textValue so they filter on the label rather than the icon’s contents.
<DropdownMenuContent align="start">
<DropdownMenuSearch placeholder="Search members..." />
<DropdownMenuLabel>Team members</DropdownMenuLabel>
<DropdownMenuItem textValue="Ada Lovelace">
<Icon icon="User" />
<span>Ada Lovelace</span>
</DropdownMenuItem>
{/* ...more members... */}
<DropdownMenuEmpty>No members found</DropdownMenuEmpty>
</DropdownMenuContent>
Always visible
Pass alwaysVisible to show the search box the moment the menu opens instead of waiting for the first keystroke. Filtering works across every item type, including checkbox and radio items. Labels and separators hide while a search is active so results stay compact, and submenu items are flattened into the main list (see below).
<DropdownMenuContent align="start">
<DropdownMenuSearch alwaysVisible placeholder="Search columns..." />
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={columns.email}
onCheckedChange={(checked) => setColumns({ ...columns, email: checked })}
onSelect={(event) => event.preventDefault()}
>
Email
</DropdownMenuCheckboxItem>
{/* ...more columns... */}
<DropdownMenuEmpty>No columns found</DropdownMenuEmpty>
</DropdownMenuContent>
Searching submenus
Items nested in a DropdownMenuSub are flattened into the main list while searching, so submenu items appear in the results without opening the submenu. Each flattened item is prefixed with its submenu path (e.g. More Tools › Save Page As…), dimmed like a keyboard shortcut so the item’s own label stays legible. Nested submenus stack the full path. Try searching for “console” below.
<DropdownMenuContent align="start">
<DropdownMenuSearch placeholder="Search actions..." />
<DropdownMenuItem>Cut</DropdownMenuItem>
<DropdownMenuItem>Copy</DropdownMenuItem>
<DropdownMenuItem>Paste</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>More Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Save Page As...</DropdownMenuItem>
<DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span>Developer Tools</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Console</DropdownMenuItem>
<DropdownMenuItem>Network</DropdownMenuItem>
<DropdownMenuItem>Task Manager</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuEmpty>No actions found</DropdownMenuEmpty>
</DropdownMenuContent>
Flattening only works when DropdownMenuSubContent is placed directly inside DropdownMenuSub — wrapping it in another element prevents its items from being searched.
Slots
| Name | Description |
|---|---|
DropdownMenu | Root component, manages open state |
DropdownMenuTrigger | Element that opens the menu on click |
DropdownMenuContent | Floating panel containing the menu items |
DropdownMenuSearch | Optional search input that filters items in place |
DropdownMenuEmpty | ”No results” row shown only when nothing matches |
DropdownMenuItem | A single actionable menu item |
DropdownMenuCheckboxItem | A toggleable item with a checkmark indicator |
DropdownMenuRadioGroup | Groups radio items into a single-select set |
DropdownMenuRadioItem | A single-select item within a radio group |
DropdownMenuLabel | Non-interactive section title |
DropdownMenuSeparator | Divider between groups of items |
DropdownMenuShortcut | Keyboard shortcut hint aligned to the item’s end |
DropdownMenuGroup | Groups related items for assistive technology |
DropdownMenuSub | Root for a nested submenu |
DropdownMenuSubTrigger | Item that opens a nested submenu |
DropdownMenuSubContent | Floating panel for a nested submenu |
Props
DropdownMenu
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
open | Controls the open state | boolean | - | ❌ |
defaultOpen | Open state when initially rendered | boolean | - | ❌ |
onOpenChange | Called when the open state changes | (open: boolean) => void | - | ❌ |
DropdownMenuTrigger
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
asChild | Merge props onto the child instead of rendering a button | boolean | false | ❌ |
DropdownMenuContent
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
align | Alignment against the trigger | start, center, end | center | ❌ |
side | Side of the trigger to render on | top, right, bottom, left | bottom | ❌ |
sideOffset | Distance in pixels from the trigger | number | 4 | ❌ |
DropdownMenuItem
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
variant | Visual style; danger marks a destructive action | neutral, danger | neutral | ❌ |
inset | Adds left padding to align with items that have icons | boolean | false | ❌ |
disabled | Prevents interaction and dims the item | boolean | false | ❌ |
textValue | Text used for search filtering; falls back to the item’s rendered text | string | - | ❌ |
onSelect | Called when the item is selected | () => void | - | ❌ |
DropdownMenuCheckboxItem
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
checked | Whether the item is checked | boolean | - | ❌ |
onCheckedChange | Called when the checked state changes | (checked: boolean) => void | - | ❌ |
disabled | Prevents interaction and dims the item | boolean | false | ❌ |
textValue | Text used for search filtering; falls back to the item’s rendered text | string | - | ❌ |
DropdownMenuRadioGroup
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
value | The value of the selected item | string | - | ❌ |
onValueChange | Called when the selected value changes | (value: string) => void | - | ❌ |
DropdownMenuRadioItem
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
value | The unique value of the item | string | - | ✅ |
disabled | Prevents interaction and dims the item | boolean | false | ❌ |
textValue | Text used for search filtering; falls back to the item’s rendered text | string | - | ❌ |
DropdownMenuLabel & DropdownMenuSubTrigger
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
inset | Adds left padding to align with items that have icons | boolean | false | ❌ |
DropdownMenuSearch
Also accepts standard input attributes, except value and onChange, which are managed internally.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
alwaysVisible | Show the input immediately instead of revealing on first keypress | boolean | false | ❌ |
placeholder | Placeholder text for the input | string | Search... | ❌ |
icon | Custom leading icon; defaults to a search icon | ReactNode | - | ❌ |
aria-label | Accessible name for the input; defaults to the placeholder text | string | placeholder | ❌ |
ref | Forwarded to the underlying <input>; null while the input is not rendered | Ref<HTMLInputElement> | - | ❌ |
DropdownMenuEmpty
Renders its children as a “no results” message, shown only while a search query matches no items. It is a live region (role="status"), so the message is announced when filtering empties the list. Also accepts standard div attributes.