Components

Dropdown menu

Radix-powered menu that opens from a trigger. The compound covers plain items, checkbox items, radio items, sub-menus, section labels, separators and keyboard shortcuts. Content chrome: rounded-md border bg-popover shadow-lg p-1 min-w-32 with a dialog-in entry animation. Items are focus:bg-muted with rounded-sm px-2 py-1.5 text-sm. The Dashboard uses this primitive under the hood for AccountMenu, row-level actions in Tables, and any custom action menu.

Updated Jul 17, 2026 by Leonardo Posada

Anatomy

Fourteen exports for full menu semantics. Core is the Root → Trigger → Content triangle; the rest are the item primitives (Item / CheckboxItem / RadioItem), the grouping primitives (Group / Label / Separator / Sub), and the affordances (Shortcut for keyboard hints, RadioGroup for exclusive picks).

  1. 1
    DropdownMenu (Root)

    Radix state container. defaultOpen / open / onOpenChange for controlled cases. modal={false} lets the user click outside without closing on touch.

  2. 2
    DropdownMenuTrigger

    The button that opens the menu. Use asChild to wrap your own Button so keyboard focus + aria-expanded flow through. In the Dashboard it's the outline Button, the icon-only button pattern from the Button 'Icon-only with tooltip' recipe, or an Avatar for account menus.

  3. 3
    DropdownMenuContent

    The floating panel. Portal + z-50 min-w-32 rounded-md border bg-popover shadow-lg p-1 with the dialog-in entry animation. Anchored to the trigger; controllable via align, side, sideOffset. Auto-focuses the first item on open.

  4. 4
    DropdownMenuItem

    A row. rounded-sm px-2 py-1.5 text-sm gap-2 cursor-pointer with focus:bg-muted. Compose a Phosphor icon on the left (16px) + text + optional Shortcut. Use inset (or the Icon variant) when the row needs pl-8 to align its leading glyph with Checkbox / Radio indicator rows.

  5. 5
    DropdownMenuCheckboxItem

    A toggleable row. Reserves pl-8 for the Check indicator on the left. checked prop controls state; onCheckedChange for the toggle.

  6. 6
    DropdownMenuRadioItem + DropdownMenuRadioGroup

    Exclusive-select rows. RadioGroup owns the value; RadioItem carries the value. Indicator is a filled primary Circle (size-2) sitting in a pl-8 left slot.

  7. 7
    DropdownMenuLabel

    Section caption. Level 1 (default) sits flush at px-2 for top-level section titles. Level 2 uses pl-8 so it aligns with the text of items that carry a leading indicator (Checkbox / Radio / Icon). Use to name a group above a few related items — never below.

  8. 8
    DropdownMenuSeparator

    Hairline divider between logical groups. -mx-1 my-1 h-px bg-border so it visually spans the full width of the content padding.

  9. 9
    DropdownMenuShortcut

    Right-aligned muted glyph for keyboard hints (⌘K, ⌘,). ml-auto text-xs tracking-widest text-muted-foreground. Do not add shortcuts users won't remember.

  10. 10
    DropdownMenuSub + SubTrigger + SubContent

    Nested menu. Trigger shows a trailing CaretRight; Sub content mirrors Content chrome. Cap nesting at one level — deeper is unreachable on touch.

States

Content: entry via animate-dialog-in (fade + slide 8px + 0.97 scale). Item: idle transparent, focus:bg-muted (keyboard + hover), disabled: opacity-50 pointer-events-none. CheckboxItem / RadioItem show the indicator only when the row is checked / selected. Trigger tracks data-[state=open] for the icon-only button pattern (border + bg while the menu is open). SubTrigger picks up bg-accent when its Sub is open. Destructive item stays text-destructive in every state — the confirm dialog behind it does the guarding.

For lists of actions — nothing else
DropdownMenu is strictly for exposing a compact list of actions triggered by a button. Anything else has a better home: single actions belong in a Button; long, searchable lists belong in a Command palette; arbitrary content (forms, tutorials) belongs in a Popover; exclusive-select in a form field belongs in a Select.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Actions list (canonical)

The default Dashboard shape: an outline Button trigger + a menu of 3–6 items. Add a Separator + a destructive-intent action (Delete rule, Remove member) as the last item when the menu ends in a delete — pair it with a confirm Dialog instead of a red button.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">Actions</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start">
    <DropdownMenuItem>
      Edit rule <DropdownMenuShortcut>⌘E</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuItem>Duplicate</DropdownMenuItem>
    <DropdownMenuItem>Rename</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Delete rule</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Icon-only trigger (row-level actions)

The pattern the Dashboard uses in Table rows: the trigger reuses the 'Icon-only with tooltip' Button recipe (native button h-8 w-8 with hover border/bg + data-[state=open] styles) so the button surface reads active while the menu is open. Icon is DotsThreeOutline (fill). Wrap the trigger in a Tooltip to label it 'Actions'. Attach the menu with align='end' so it hugs the row's right edge.

<DropdownMenu>
  <Tooltip>
    <TooltipTrigger asChild>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          aria-label="Actions"
          className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition-all hover:border-border hover:bg-background hover:text-foreground hover:shadow-xs data-[state=open]:border-border data-[state=open]:bg-background data-[state=open]:text-foreground"
        >
          <DotsThreeOutline weight="fill" className="size-4" />
        </button>
      </DropdownMenuTrigger>
    </TooltipTrigger>
    <TooltipContent side="top" sideOffset={6}>Actions</TooltipContent>
  </Tooltip>
  <DropdownMenuContent align="end">
    <DropdownMenuItem>Edit</DropdownMenuItem>
    <DropdownMenuItem>Duplicate</DropdownMenuItem>
    <DropdownMenuItem>Delete</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Avatar + text trigger (account / workspace)

The workspace / account switcher pattern: a 48px-tall trigger with border rounded-md shadow-xs, an Avatar-32 on the left, a two-line text stack (name font-medium 14/20, description text-muted-foreground text-xs), and a trailing CaretUpDown 16px at 50% opacity. Wire it to a menu that leads with a User item (Avatar + name + email) followed by workspace / settings / logout sections split by Separators. Hover moves to bg-accent; focus keeps the ring; disabled goes opacity-50.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <button
      type="button"
      className="flex h-12 w-56 items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-sm shadow-xs transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent"
    >
      <Avatar size="md">
        <AvatarFallback>L</AvatarFallback>
      </Avatar>
      <div className="flex min-w-0 flex-1 flex-col">
        <span className="truncate font-medium text-foreground">Leonardo</span>
        <span className="truncate text-xs text-muted-foreground">leonardo@y.uno</span>
      </div>
      <CaretUpDown weight="light" className="size-4 opacity-50" />
    </button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" className="w-56">
    <div className="flex items-center gap-2 px-1 py-1.5">
      <Avatar size="md">
        <AvatarFallback>L</AvatarFallback>
      </Avatar>
      <div className="flex min-w-0 flex-1 flex-col">
        <span className="truncate text-sm font-semibold text-foreground">Leonardo</span>
        <span className="truncate text-xs text-muted-foreground">leonardo@y.uno</span>
      </div>
    </div>
    <DropdownMenuSeparator />
    <DropdownMenuGroup>
      <DropdownMenuItem>Profile</DropdownMenuItem>
      <DropdownMenuItem>Workspace settings</DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Sections + separators

For account menus and settings — group related items under a Label caption and separate groups with a Separator. Use Label level=2 (pl-8) when the group sits inside a menu that already reserves the indicator gutter so the caption aligns with the items' text. Never end the menu on a Label; every section must contain at least one interactive item.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">Account</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" className="w-56">
    <DropdownMenuLabel>Account</DropdownMenuLabel>
    <DropdownMenuItem>Profile</DropdownMenuItem>
    <DropdownMenuItem>
      Settings <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuLabel>Team</DropdownMenuLabel>
    <DropdownMenuItem>Invite members</DropdownMenuItem>
    <DropdownMenuItem>Billing</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Checkbox items (multi-select preferences)

Column visibility, notification categories, filter tags — anything the user picks multiple of. Use controlled state (checked + onCheckedChange) so the parent tracks the selection. Keep the menu open between clicks — that's Radix default.

function ColumnVisibility() {
  const [status, setStatus] = React.useState(true);
  const [country, setCountry] = React.useState(true);
  const [amount, setAmount] = React.useState(true);
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">
          <Funnel weight="light" className="size-4" />
          Columns
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-56">
        <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuCheckboxItem checked={status} onCheckedChange={setStatus}>
          Status
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem checked={country} onCheckedChange={setCountry}>
          Country
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem checked={amount} onCheckedChange={setAmount}>
          Amount
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
Radio items (exclusive picker)

For picking one option — sort order, view density, chart scale. Wrap the items in a RadioGroup so Radix enforces the exclusivity and manages the value.

function SortBy() {
  const [value, setValue] = React.useState("date-desc");
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Sort by</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-56">
        <DropdownMenuLabel>Sort by</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuRadioGroup value={value} onValueChange={setValue}>
          <DropdownMenuRadioItem value="date-desc">Date · Newest first</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="date-asc">Date · Oldest first</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="amount">Amount</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="status">Status</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
Icon items (leading glyph)

For menus where every action has a semantic pair (User, Gear, ArrowUpRight, SignOut). Put a Phosphor icon 16px at the pl-8 slot so items align with any Checkbox / Radio row you may also render. Icons are Phosphor weight=light by default and follow the Yuno two-weight rule.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">More</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" className="w-56">
    <DropdownMenuItem className="pl-8">
      <User weight="light" className="absolute left-2 size-4" />
      Profile
    </DropdownMenuItem>
    <DropdownMenuItem className="pl-8">
      <Gear weight="light" className="absolute left-2 size-4" />
      Settings
    </DropdownMenuItem>
    <DropdownMenuItem className="pl-8">
      <ArrowUpRight weight="light" className="absolute left-2 size-4" />
      Open in new tab
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem className="pl-8">
      <SignOut weight="light" className="absolute left-2 size-4" />
      Log out
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Destructive item (delete with confirm)

Yuno reserves red for genuinely destructive actions (Delete account, Remove team). Use text-destructive on the item's text + icon; keep the row background transparent (no red fill, no red hover). The click MUST open a confirm Dialog — never mutate on the direct click.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">Actions</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start">
    <DropdownMenuItem>Rename</DropdownMenuItem>
    <DropdownMenuItem>Duplicate</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem
      className="text-destructive focus:text-destructive"
      onSelect={() => openConfirmDialog()}
    >
      Delete rule
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
Sub-menu (step-in options)

One level of nesting for options that need their own micro-list (Move to → Folder A / Folder B, Share → Email / Copy link). SubTrigger shows the CaretRight; SubContent opens to the right by default. Cap at one level.

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="outline">Share</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" className="w-56">
    <DropdownMenuItem>Copy link</DropdownMenuItem>
    <DropdownMenuSub>
      <DropdownMenuSubTrigger>Send via</DropdownMenuSubTrigger>
      <DropdownMenuSubContent>
        <DropdownMenuItem>Email</DropdownMenuItem>
        <DropdownMenuItem>Slack</DropdownMenuItem>
        <DropdownMenuItem>WhatsApp</DropdownMenuItem>
      </DropdownMenuSubContent>
    </DropdownMenuSub>
    <DropdownMenuItem>Export as CSV</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Import

Full compound import from @/components/ui/dropdown-menu.

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuRadioGroup,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
} from "@/components/ui/dropdown-menu";

Props

Radix Dropdown menu props on each part. The most-used in prototypes:

PropTypeDefaultDescription
DropdownMenu.open / defaultOpenbooleanControlled or uncontrolled open state. Controlled needs onOpenChange.
DropdownMenu.onOpenChange(open: boolean) => voidFires whenever the menu opens or closes.
DropdownMenu.modalbooleantrueWhen true (Radix default), interaction outside the menu is blocked. Set false for menus that should coexist with the page (e.g. hover panels).
DropdownMenuTrigger.asChildbooleanfalseMerges props onto the child element (Radix Slot). Always pass when wrapping a real Button so keyboard focus + aria-expanded flow through.
DropdownMenuContent.side"top" | "right" | "bottom" | "left""bottom"Preferred side. Radix auto-flips if there's not enough room.
DropdownMenuContent.align"start" | "center" | "end""center"Alignment along the side. Row-action menus use "end" to hug the row's right edge; account menus use "start".
DropdownMenuContent.sideOffsetnumber4Distance in px between trigger and content.
DropdownMenuContent.classNamestringMerged. Default is min-w-32. Set w-56 or wider for menus with checkbox/radio content so labels fit without truncation.
DropdownMenuItem.insetbooleanfalseAdds pl-8 so plain items align with checkbox/radio rows that reserve a left indicator slot.
DropdownMenuItem.onSelect(event: Event) => voidFires when the item is chosen (keyboard or click). Call event.preventDefault() to keep the menu open after selecting.
DropdownMenuItem.disabledbooleanfalseDisables the item — opacity-50 + pointer-events-none.
DropdownMenuCheckboxItem.checked / onCheckedChangeboolean / (checked: boolean) => voidControlled state. Wrapping in useState is enough for prototypes.
DropdownMenuRadioGroup.value / onValueChangestring / (value: string) => voidControlled group value. Each RadioItem carries its own value prop.
DropdownMenuLabel.insetbooleanfalseAdds pl-8 so the caption aligns with rows that carry an indicator (Figma 'Label / Level=2').
DropdownMenuSubcomponentWrap a SubTrigger + SubContent to nest one level. Never nest deeper — the second sub-menu is unreachable on touch.

When to use

  • Row-level actions in a Table (Edit / Duplicate / Delete / Open) — the Dashboard's canonical Dots + DropdownMenu pattern.
  • Account menus and settings entry points anchored to a Button or Avatar.
  • Bulk actions on selected rows (Delete selected, Export, Tag).
  • Preferences with multiple togglable options (column visibility, notification categories).
  • Exclusive one-of-many pickers where a Select would feel too heavy (sort order, chart scale).

When not to use

  • For a single action — just use a Button.
  • For form fields where the user picks a value — use Select.
  • For long or searchable lists — use Command.
  • For arbitrary content (forms, tutorial popovers) — use Popover.
  • For right-click / canvas menus — use Context menu (same compound but triggered by a right click).

Usage

Do
  • Group related items under a DropdownMenuLabel and split groups with a Separator.
  • Keep menus short — 3–7 items reads well; longer lists belong in Command.
  • Anchor row-action menus with align='end' so they hug the row's right edge.
  • For row actions use the icon-only trigger + Tooltip 'Actions' — reuse Button's Icon-only recipe.
  • Use shortcuts sparingly and only for actions the user repeats.
Don't
  • Don't nest more than one sub-menu level — the second level is unreachable on touch.
  • Don't mix Checkbox items and Radio items in the same visual group — they collide semantically.
  • Don't end a menu on a Label — every section needs at least one interactive item.
  • Don't paint a destructive item's background red — Yuno destructive items keep bg transparent, only the text + icon go text-destructive. The confirm Dialog behind it does the guarding.
  • Don't use DropdownMenu for arbitrary content (forms, tutorial) — that's a Popover.

Related

Cross-links to atoms and patterns you may reach for next.

  • Context menuSame compound API but right-click triggered. Use for canvas-level actions, not row actions.
  • PopoverReach for Popover when the content is arbitrary (form snippet, tutorial). DropdownMenu is only for lists of actions.
  • CommandUse Command when the list is long enough to search. Dropdown menus should stay short.
  • ButtonThe icon-only trigger reuses Button's 'Icon-only with tooltip' recipe treatment.
  • TooltipWrap icon-only triggers in a Tooltip so the button has a discoverable label.
  • AvatarThe Avatar + text trigger uses Avatar size-8 with a text stack + CaretUpDown for the account / workspace switcher.
  • Account menuThe organism that composes the Avatar + text trigger recipe with a User item, workspace picker and logout section.