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

### 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";
```

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

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

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

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

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

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

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