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.

VariantLeft slotRight slot
DropdownMenuItemprefix (icon)suffix / shortcut / selected checkmark
DropdownMenuCheckboxItemCheckbox (auto)Optional checkmark when checked (off by default in pickers)
DropdownMenuRadioItemCheckmark when selected
DropdownMenuSubTriggerChevron (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

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

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

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

tsx
import { IconMagnifyingGlass } from "@rogo-technologies/ui/icons";

<DropdownMenuSearch
  value={query}
  onChange={(e) => setQuery(e.target.value)}
  prefix={<IconMagnifyingGlass className="text-tertiary size-4" />}
/>;
tsx
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.

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

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

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

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

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

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

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

tsx
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

tsx
<DropdownMenuContent side="right" align="start" sideOffset={8}></DropdownMenuContent>
<DropdownMenuContent side="top" align="center"></DropdownMenuContent>

Accessibility

  • Menu uses role="menu" with role="menuitem" children
  • Keyboard: / to move, Enter/Space to select, Esc to 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" within role="group"
  • DropdownMenuSearch is a plain text input — arrow keys still navigate items so keyboard users can search and select without leaving the keyboard

API

DropdownMenu

PropTypeDefaultDescription
openbooleanControlled open state.
defaultOpenbooleanfalseInitial open state for uncontrolled usage.
onOpenChange(open: boolean) => voidCallback when open state changes.

DropdownMenuTrigger

PropTypeDefaultDescription
asChildbooleanfalseRender as the child element instead of a button.

DropdownMenuContent

Convenience wrapper that renders Portal → Positioner → Popup.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Side of the trigger to anchor.
sideOffsetnumber4Offset from the trigger edge in px.
align"start" | "center" | "end""start"Alignment along the side axis.
alignOffsetnumberOffset from the alignment edge.
collisionPaddingnumber | PaddingPadding from viewport edges for collision detection.
positionerClassNamestringAdditional class for the positioner wrapper.

DropdownMenuSearch

PropTypeDefaultDescription
valuestringControlled input value.
onChangehandlerFired when the user types.
placeholderstring"Search options…"Placeholder text.
autoFocusOnMountbooleantrueFocuses the input as soon as the popup mounts.
prefixReactNodeSlot rendered before the input (e.g. a search icon).
suffixReactNodeSlot rendered after the input (e.g. a clear button).

DropdownMenuItem

PropTypeDefaultDescription
selectedbooleanfalseDraws a checkmark in the right slot. Use for single-select rows.
prefixReactNodeLeft-slot content (icon, badge, custom checkbox).
suffixReactNodeRight-slot content. Overrides the selected checkmark.
insetbooleanfalseAdds left padding to align with items that have icons.
actionType"default" | "destructive""default"Visual style — destructive shows red text.
onSelect(event: MouseEvent) => voidCallback when selected (mapped to onClick).
tooltipReactNodeShown 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.
disabledbooleanfalseDisables the item.

DropdownMenuCheckboxItem

PropTypeDefaultDescription
checkedbooleanControlled checked state.
onCheckedChange(checked: boolean) => voidCallback when checked state changes.
closeOnClickbooleantrueSet to false to keep the menu open while toggling multiple options.
showSelectedIndicatorbooleantrueRight-slot confirming checkmark. Set to false for left-checkbox only — the canonical multi-select pattern.

DropdownMenuRadioGroup

PropTypeDefaultDescription
valuestringControlled selected value.
onValueChange(value: string) => voidCallback when selection changes.

DropdownMenuRadioItem

PropTypeDefaultDescription
valuestringUnique value identifying this item.

DropdownMenuSubTrigger

PropTypeDefaultDescription
insetbooleanfalseAdds left padding for alignment.
openOnHoverbooleantrueOpens the submenu when the trigger is hovered or keyboard-highlighted. Focus stays on the parent trigger until the user presses Right Arrow / Enter.
delaynumber100Hover delay in ms before the submenu opens.

DropdownMenuSubContent

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"Side relative to the sub-trigger.
sideOffsetnumber-2Offset from the sub-trigger edge.
align"start" | "center" | "end""start"Alignment along the side axis.

DropdownMenuLabel

PropTypeDefaultDescription
insetbooleanfalseAdds left padding for alignment.

Other subcomponents

ComponentPurpose
DropdownMenuGroupWraps a related set of items.
DropdownMenuSeparatorFull-width divider between groups of items.
DropdownMenuShortcutRight-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 selected on DropdownMenuItem for single-select pickers (filters with one value)
  • Use DropdownMenuCheckboxItem for multi-select pickers (filters with many values)
  • Use DropdownMenuRadioGroup when the values are a fixed enum (sort order, view mode)
  • Add a DropdownMenuSearch when 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-tertiary so they don't compete with the label (destructive items skip text-tertiary so the icon takes the destructive color)
  • Style your trigger's open state via the data-popup-open attribute 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 selected checkmark and a custom suffix on the same DropdownMenuItemsuffix wins
PreviousDialog
NextInput
Made in NYC© 2026 Rogo Technologies Inc.