Equality
Equality

Dropdown Menu

A menu of actions or options triggered by a button

View Markdown

Overview

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

NameDescription
DropdownMenuRoot component, manages open state
DropdownMenuTriggerElement that opens the menu on click
DropdownMenuContentFloating panel containing the menu items
DropdownMenuSearchOptional search input that filters items in place
DropdownMenuEmpty”No results” row shown only when nothing matches
DropdownMenuItemA single actionable menu item
DropdownMenuCheckboxItemA toggleable item with a checkmark indicator
DropdownMenuRadioGroupGroups radio items into a single-select set
DropdownMenuRadioItemA single-select item within a radio group
DropdownMenuLabelNon-interactive section title
DropdownMenuSeparatorDivider between groups of items
DropdownMenuShortcutKeyboard shortcut hint aligned to the item’s end
DropdownMenuGroupGroups related items for assistive technology
DropdownMenuSubRoot for a nested submenu
DropdownMenuSubTriggerItem that opens a nested submenu
DropdownMenuSubContentFloating panel for a nested submenu

Props

NameDescriptionTypeDefaultRequired
openControls the open stateboolean-
defaultOpenOpen state when initially renderedboolean-
onOpenChangeCalled when the open state changes(open: boolean) => void-
NameDescriptionTypeDefaultRequired
asChildMerge props onto the child instead of rendering a buttonbooleanfalse
NameDescriptionTypeDefaultRequired
alignAlignment against the triggerstart, center, endcenter
sideSide of the trigger to render ontop, right, bottom, leftbottom
sideOffsetDistance in pixels from the triggernumber4
NameDescriptionTypeDefaultRequired
variantVisual style; danger marks a destructive actionneutral, dangerneutral
insetAdds left padding to align with items that have iconsbooleanfalse
disabledPrevents interaction and dims the itembooleanfalse
textValueText used for search filtering; falls back to the item’s rendered textstring-
onSelectCalled when the item is selected() => void-
NameDescriptionTypeDefaultRequired
checkedWhether the item is checkedboolean-
onCheckedChangeCalled when the checked state changes(checked: boolean) => void-
disabledPrevents interaction and dims the itembooleanfalse
textValueText used for search filtering; falls back to the item’s rendered textstring-
NameDescriptionTypeDefaultRequired
valueThe value of the selected itemstring-
onValueChangeCalled when the selected value changes(value: string) => void-
NameDescriptionTypeDefaultRequired
valueThe unique value of the itemstring-
disabledPrevents interaction and dims the itembooleanfalse
textValueText used for search filtering; falls back to the item’s rendered textstring-
NameDescriptionTypeDefaultRequired
insetAdds left padding to align with items that have iconsbooleanfalse

Also accepts standard input attributes, except value and onChange, which are managed internally.

NameDescriptionTypeDefaultRequired
alwaysVisibleShow the input immediately instead of revealing on first keypressbooleanfalse
placeholderPlaceholder text for the inputstringSearch...
iconCustom leading icon; defaults to a search iconReactNode-
aria-labelAccessible name for the input; defaults to the placeholder textstringplaceholder
refForwarded to the underlying <input>; null while the input is not renderedRef<HTMLInputElement>-

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.