---
title: "Dropdown Menu"
description: "A menu of actions or options triggered by a button"
source: "Equality"
---
import { DropdownMenuDemo } from "@demo/components/demo/dropdown-menu";

## 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:

```ts
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
} from "@eqtylab/equality";
```

Basic usage:

```jsx
<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>
```

<DropdownMenuDemo client:only="react" />

## Variants

### Item Variants

`DropdownMenuItem` takes a `variant` to mark an action's weight. Use `danger` for destructive or irreversible actions, and `warning` for actions that are consequential but recoverable — something the user should pause over rather than avoid. Leave everything else `neutral`.

Colour alone shouldn't carry the meaning, so keep the label explicit about what the action does.

<DropdownMenuDemo client:only="react" variant="item-variants" />

```jsx
<DropdownMenuContent align="start">
  <DropdownMenuItem>View logs</DropdownMenuItem>
  <DropdownMenuItem>Duplicate</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="warning">
    Roll back to previous release
  </DropdownMenuItem>
  <DropdownMenuItem variant="danger">Delete deployment</DropdownMenuItem>
</DropdownMenuContent>
```

### 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.

<DropdownMenuDemo client:only="react" variant="with-separators" />

```jsx
<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`.

<DropdownMenuDemo client:only="react" variant="with-checkboxes" />

```jsx
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`.

<DropdownMenuDemo client:only="react" variant="with-radio" />

```jsx
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.

<DropdownMenuDemo client:only="react" variant="with-shortcuts" />

```jsx
<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.

<DropdownMenuDemo client:only="react" variant="with-submenu" />

```jsx
<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.

<DropdownMenuDemo client:only="react" variant="with-groups" />

```jsx
<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:

```ts
import { DropdownMenuSearch, DropdownMenuEmpty } from "@eqtylab/equality";
```

### Default

The search box shows, focused, the moment the menu opens. 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). Add a `DropdownMenuEmpty` to show a "no results" row when nothing matches.

<DropdownMenuDemo client:only="react" variant="with-search" />

```jsx
<DropdownMenuContent align="start">
  <DropdownMenuSearch placeholder="Search columns..." />
  <DropdownMenuEmpty>No columns found</DropdownMenuEmpty>
  <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
  <DropdownMenuCheckboxItem
    checked={columns.email}
    onCheckedChange={(checked) => setColumns({ ...columns, email: checked })}
    onSelect={(event) => event.preventDefault()}
  >
    Email
  </DropdownMenuCheckboxItem>
  {/* ...more columns... */}
</DropdownMenuContent>
```

### Reveal on typing

Pass `alwaysVisible={false}` to keep the search box hidden until someone types into the open menu — the first keystroke reveals it and seeds the query. Nothing on screen says search exists, so keep this for compact, keyboard-heavy menus. Give items that lead with an icon or avatar a `textValue` so they filter on the label rather than the icon's contents.

<DropdownMenuDemo client:only="react" variant="with-search-reveal" />

```jsx
<DropdownMenuContent align="start">
  <DropdownMenuSearch alwaysVisible={false} placeholder="Search members..." />
  <DropdownMenuEmpty>No members found</DropdownMenuEmpty>
  <DropdownMenuLabel>Team members</DropdownMenuLabel>
  <DropdownMenuItem textValue="Ada Lovelace">
    <Icon icon="User" />
    <span>Ada Lovelace</span>
  </DropdownMenuItem>
  {/* ...more members... */}
</DropdownMenuContent>
```

### Persistent items

Mark an item `persistent` to keep it on screen while searching, such as a footer action that belongs to the whole list. It is never a result: it doesn't count towards `DropdownMenuEmpty` or the announced result count, and <kbd>Enter</kbd> in the search box skips it. A `persistent` separator likewise stays visible.

To act on only what the search shows, call `useDropdownMenuSearchQuery()` from a component inside the menu. It returns the `query` as typed, `isSearching` (whether a non-blank query is active), and `matches(text)`, which applies the same rule the items use. Try searching for "e" below, then choosing "Hide all".

<DropdownMenuDemo client:only="react" variant="with-search-persistent" />

```jsx
import { useDropdownMenuSearchQuery } from "@eqtylab/equality";

const ColumnVisibilityFooter = ({ onShow }) => {
  const { matches } = useDropdownMenuSearchQuery();
  const shown = Object.keys(COLUMN_LABELS).filter((key) =>
    matches(COLUMN_LABELS[key]),
  );

  return (
    <>
      <DropdownMenuSeparator persistent />
      <DropdownMenuItem
        persistent
        onSelect={(event) => {
          event.preventDefault();
          onShow(shown, true);
        }}
      >
        Show all
      </DropdownMenuItem>
      <DropdownMenuItem
        persistent
        onSelect={(event) => {
          event.preventDefault();
          onShow(shown, false);
        }}
      >
        Hide all
      </DropdownMenuItem>
    </>
  );
};
```

Render it inside `DropdownMenuContent`, after the items it acts on:

```jsx
<DropdownMenuContent align="start">
  <DropdownMenuSearch placeholder="Search columns..." />
  <DropdownMenuEmpty>No columns found</DropdownMenuEmpty>
  {Object.entries(COLUMN_LABELS).map(([key, label]) => (
    <DropdownMenuCheckboxItem
      key={key}
      checked={columns[key]}
      onCheckedChange={(checked) =>
        setColumns((prev) => ({ ...prev, [key]: checked }))
      }
      onSelect={(event) => event.preventDefault()}
    >
      {label}
    </DropdownMenuCheckboxItem>
  ))}
  <ColumnVisibilityFooter
    onShow={(keys, visible) =>
      setColumns((prev) => ({
        ...prev,
        ...Object.fromEntries(keys.map((key) => [key, visible])),
      }))
    }
  />
</DropdownMenuContent>
```

`useDropdownMenuSearchQuery()` only works inside a `DropdownMenu` (it reads the search via context) and throws otherwise.

### 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.

<DropdownMenuDemo client:only="react" variant="with-search-submenu" />

```jsx
<DropdownMenuContent align="start">
  <DropdownMenuSearch placeholder="Search actions..." />
  <DropdownMenuEmpty>No actions found</DropdownMenuEmpty>
  <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>
</DropdownMenuContent>
```

Flattening only works when `DropdownMenuSubContent` is placed directly inside `DropdownMenuSub` — wrapping it in another element prevents its items from being searched.

### Keyboard

<kbd>Enter</kbd> in the search box activates the first matching item, skipping
`persistent` ones.
<kbd>↓</kbd> moves from the search box to the first item and <kbd>↑</kbd> to the
last; <kbd>↑</kbd> on the first item and <kbd>↓</kbd> on the last return to the
search box, so the arrows cycle through it. Typing or
<kbd>Backspace</kbd> while an item is focused keeps editing the query.

### Accessibility

The search box and its live regions sit inside the menu, which Radix gives the `menu` role. ARIA only allows menu items, groups and separators there, so automated checkers such as axe report `aria-required-children`, and some screen readers skip the search box in browse mode. They can't move outside the menu: while it is open, Radix hides everything outside it from assistive technology.

To pick a value in a form rather than run an action, use a [Select with search](/components/select#search--filtering).

## 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; `warning` marks a consequential action, `danger` a destructive one | `neutral`, `warning`, `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`                       | -         | ❌       |
| `persistent` | Stays visible while searching, and is never a result                             | `boolean`                      | `false`   | ❌       |
| `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`                     | -       | ❌       |
| `persistent`      | Stays visible while searching, and is never a result                   | `boolean`                    | `false` | ❌       |

### 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`  | -       | ❌       |
| `persistent` | Stays visible while searching, and is never a result                   | `boolean` | `false` | ❌       |

### DropdownMenuLabel & DropdownMenuSubTrigger

| Name    | Description                                           | Type      | Default | Required |
| ------- | ----------------------------------------------------- | --------- | ------- | -------- |
| `inset` | Adds left padding to align with items that have icons | `boolean` | `false` | ❌       |

### DropdownMenuSeparator

| Name         | Description                   | Type      | Default | Required |
| ------------ | ----------------------------- | --------- | ------- | -------- |
| `persistent` | Stays visible while searching | `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 on open; `false` reveals it on the first keystroke instead     | `boolean`               | `true`        | ❌       |
| `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.