Dropdown Menu

Displays a menu of actions or functions, opened from a button.

import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

const styles = stylex.create({
  menu: { minWidth: "10rem" },
})

export default function DropdownMenuDemo() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent xstyle={styles.menu}>
        <DropdownMenuGroup>
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuItem>
            Profile
            <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Billing
            <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Settings
            <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>Team</DropdownMenuItem>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
            <DropdownMenuSubContent>
              <DropdownMenuItem>Email</DropdownMenuItem>
              <DropdownMenuItem>Message</DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem>More...</DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuItem>
            New Team
            <DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>GitHub</DropdownMenuItem>
          <DropdownMenuItem>Support</DropdownMenuItem>
          <DropdownMenuItem disabled>API</DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          Log out
          <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx lucide-react

Set up the token layer and the StyleX compiler, if you have not already. See Installation, and paste states.ts and state-styles.ts from the same page. states.ts holds the popup surface and the highlighted and dimmed item styles. The items and the submenu trigger read their Base UI state through state-styles.ts. Include interop.css for item icon sizing and the open background on button triggers.

Copy and paste the following code into your project.

components/ui/dropdown-menu.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import type { CSSProperties } from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { Check, ChevronRight } from "lucide-react"

import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import {
  color,
  duration,
  ease,
  layer,
  leading,
  radius,
  shadow,
  space,
  text,
  tracking,
} from "@/theme/tokens.stylex"

const styles = stylex.create({
  hidden: { opacity: 0 },
  content: {
    zIndex: layer.popup,
    maxHeight: "var(--available-height)",
    minWidth: "max(8rem, var(--anchor-width))",
    overflowX: "hidden",
    overflowY: "auto",
    "--carbon-popup-radius": radius.lg,
    borderRadius: "var(--carbon-popup-radius)",
    padding: space.s1,
    transformOrigin: "var(--transform-origin)",
    transitionProperty: "opacity, transform",
    transitionDuration: {
      default: duration.fade,
      "@media (prefers-reduced-motion: reduce)": "0s",
    },
    transitionTimingFunction: ease.standard,
  },
  subContent: {
    width: "auto",
    minWidth: "6rem",
    boxShadow: shadow.popupLifted,
  },
  label: {
    paddingInline: space.s1_5,
    paddingBlock: space.s1,
    fontSize: text.xs,
    lineHeight: leading.xs,
    fontWeight: 500,
    color: color.mutedForeground,
  },
  item: {
    position: "relative",
    display: "flex",
    alignItems: "center",
    gap: space.s1_5,
    cursor: "default",
    userSelect: "none",
    paddingInline: space.s1_5,
    paddingBlock: space.s1,
    fontSize: text.sm,
    lineHeight: leading.sm,
    outlineStyle: "none",
    "--carbon-icon-size": space.s4,
  },
  inset: { paddingInlineStart: space.s7 },
  destructive: { color: color.destructive },
  indicatorItem: { paddingInlineEnd: space.s8 },
  indicator: {
    pointerEvents: "none",
    position: "absolute",
    insetInlineEnd: space.s2,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
  },
  chevron: { marginInlineStart: "auto" },
  separator: {
    marginInline: `calc(-1 * ${space.s1})`,
    marginBlock: space.s1,
    height: 1,
    backgroundColor: color.border,
  },
  shortcut: {
    marginInlineStart: "auto",
    fontSize: text.xs,
    lineHeight: leading.xs,
    letterSpacing: tracking.widest,
    color: color.mutedForeground,
  },
})

type ItemVariant = "default" | "destructive"

const slide = stylex.create({
  top: { transform: `scale(0.95) translateY(${space.s2})` },
  bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
  left: { transform: `scale(0.95) translateX(${space.s2})` },
  right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
  "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
  "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
})

type Overrides = { xstyle?: StyleXStyles; className?: string }

type StateOverrides = Overrides & { style?: CSSProperties }

function DropdownMenu(props: MenuPrimitive.Root.Props) {
  return <MenuPrimitive.Root {...props} />
}

function DropdownMenuPortal(props: MenuPrimitive.Portal.Props) {
  return <MenuPrimitive.Portal {...props} />
}

function DropdownMenuTrigger(props: MenuPrimitive.Trigger.Props) {
  return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}

type DropdownMenuContentProps = Omit<MenuPrimitive.Popup.Props, "className" | "style"> &
  Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
  StateOverrides

function DropdownMenuContent({
  xstyle,
  style,
  className,
  side = "bottom",
  sideOffset = 4,
  align = "start",
  alignOffset = 0,
  ...props
}: DropdownMenuContentProps) {
  return (
    <MenuPrimitive.Portal>
      <MenuPrimitive.Positioner
        align={align}
        alignOffset={alignOffset}
        side={side}
        sideOffset={sideOffset}
        {...stylex.props(states.positioner)}
      >
        <MenuPrimitive.Popup
          data-slot="dropdown-menu-content"
          {...props}
          {...stateStyles(
            (state) => [
              states.popupSurface,
              styles.content,
              (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                styles.hidden,
              (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                slide[state.side],
              xstyle,
            ],
            { className, style },
          )}
        />
      </MenuPrimitive.Positioner>
    </MenuPrimitive.Portal>
  )
}

function DropdownMenuGroup(props: MenuPrimitive.Group.Props) {
  return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}

type DropdownMenuLabelProps = Omit<MenuPrimitive.GroupLabel.Props, "className"> &
  Overrides & { inset?: boolean }

function DropdownMenuLabel({ xstyle, className, inset, ...props }: DropdownMenuLabelProps) {
  const sx = stylex.props(styles.label, inset && styles.inset, xstyle)
  return (
    <MenuPrimitive.GroupLabel
      data-slot="dropdown-menu-label"
      data-inset={inset || undefined}
      {...sx}
      {...props}
      className={clsx(sx.className, className)}
    />
  )
}

type DropdownMenuItemProps = Omit<MenuPrimitive.Item.Props, "className" | "style"> &
  StateOverrides & { inset?: boolean; variant?: ItemVariant }

function DropdownMenuItem({
  xstyle,
  style,
  className,
  inset,
  variant = "default",
  ...props
}: DropdownMenuItemProps) {
  const destructive = variant === "destructive"
  return (
    <MenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-inset={inset || undefined}
      data-variant={variant}
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          states.popupItem,
          inset && styles.inset,
          destructive && styles.destructive,
          state.highlighted &&
            (destructive ? states.itemHighlightDestructive : states.itemHighlight),
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}

type DropdownMenuLinkItemProps = Omit<MenuPrimitive.LinkItem.Props, "className" | "style"> &
  StateOverrides & { inset?: boolean }

function DropdownMenuLinkItem({
  xstyle,
  style,
  className,
  inset,
  ...props
}: DropdownMenuLinkItemProps) {
  return (
    <MenuPrimitive.LinkItem
      data-slot="dropdown-menu-link-item"
      data-inset={inset || undefined}
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          states.popupItem,
          inset && styles.inset,
          state.highlighted && states.itemHighlight,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}

type DropdownMenuCheckboxItemProps = Omit<MenuPrimitive.CheckboxItem.Props, "className" | "style"> &
  StateOverrides & { inset?: boolean }

function DropdownMenuCheckboxItem({
  xstyle,
  style,
  className,
  inset,
  children,
  ...props
}: DropdownMenuCheckboxItemProps) {
  return (
    <MenuPrimitive.CheckboxItem
      data-slot="dropdown-menu-checkbox-item"
      data-inset={inset || undefined}
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          states.popupItem,
          styles.indicatorItem,
          inset && styles.inset,
          state.highlighted && states.itemHighlight,
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    >
      <span data-slot="dropdown-menu-checkbox-item-indicator" {...stylex.props(styles.indicator)}>
        <MenuPrimitive.CheckboxItemIndicator>
          <Check />
        </MenuPrimitive.CheckboxItemIndicator>
      </span>
      {children}
    </MenuPrimitive.CheckboxItem>
  )
}

function DropdownMenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
  return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
}

type DropdownMenuRadioItemProps = Omit<MenuPrimitive.RadioItem.Props, "className" | "style"> &
  StateOverrides & { inset?: boolean }

function DropdownMenuRadioItem({
  xstyle,
  style,
  className,
  inset,
  children,
  ...props
}: DropdownMenuRadioItemProps) {
  return (
    <MenuPrimitive.RadioItem
      data-slot="dropdown-menu-radio-item"
      data-inset={inset || undefined}
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          states.popupItem,
          styles.indicatorItem,
          inset && styles.inset,
          state.highlighted && states.itemHighlight,
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    >
      <span data-slot="dropdown-menu-radio-item-indicator" {...stylex.props(styles.indicator)}>
        <MenuPrimitive.RadioItemIndicator>
          <Check />
        </MenuPrimitive.RadioItemIndicator>
      </span>
      {children}
    </MenuPrimitive.RadioItem>
  )
}

type DropdownMenuSeparatorProps = Omit<MenuPrimitive.Separator.Props, "className"> & Overrides

function DropdownMenuSeparator({ xstyle, className, ...props }: DropdownMenuSeparatorProps) {
  const sx = stylex.props(styles.separator, xstyle)
  return (
    <MenuPrimitive.Separator
      data-slot="dropdown-menu-separator"
      {...sx}
      {...props}
      className={clsx(sx.className, className)}
    />
  )
}

type DropdownMenuShortcutProps = useRender.ComponentProps<"span"> & Overrides

function DropdownMenuShortcut({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: DropdownMenuShortcutProps) {
  const sx = stylex.props(styles.shortcut, xstyle)
  return useRender({
    defaultTagName: "span",
    render,
    ref,
    props: {
      "data-slot": "dropdown-menu-shortcut",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

function DropdownMenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
  return <MenuPrimitive.SubmenuRoot {...props} />
}

type DropdownMenuSubTriggerProps = Omit<MenuPrimitive.SubmenuTrigger.Props, "className" | "style"> &
  StateOverrides & { inset?: boolean }

function DropdownMenuSubTrigger({
  xstyle,
  style,
  className,
  inset,
  children,
  ...props
}: DropdownMenuSubTriggerProps) {
  return (
    <MenuPrimitive.SubmenuTrigger
      data-slot="dropdown-menu-sub-trigger"
      data-inset={inset || undefined}
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          states.popupItem,
          inset && styles.inset,
          (state.highlighted || state.open) && states.itemHighlight,
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    >
      {children}
      <ChevronRight {...stylex.props(styles.chevron)} />
    </MenuPrimitive.SubmenuTrigger>
  )
}

function DropdownMenuSubContent({
  xstyle,
  side = "right",
  sideOffset = 0,
  align = "start",
  alignOffset = -3,
  ...props
}: DropdownMenuContentProps) {
  return (
    <DropdownMenuContent
      data-slot="dropdown-menu-sub-content"
      side={side}
      sideOffset={sideOffset}
      align={align}
      alignOffset={alignOffset}
      xstyle={[styles.subContent, xstyle]}
      {...props}
    />
  )
}

export {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuLinkItem,
  DropdownMenuPortal,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
}

Update the import paths to match your project setup.

Usage

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
<DropdownMenu>
  <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuGroup>
      <DropdownMenuLabel>My Account</DropdownMenuLabel>
      <DropdownMenuItem>Profile</DropdownMenuItem>
      <DropdownMenuItem>Billing</DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Pass the trigger element through render. Base UI merges aria-haspopup and aria-expanded into it and the Button keeps its styles.

Basic

Use DropdownMenuLabel to name a group and DropdownMenuSeparator to divide groups.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function DropdownMenuBasic() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuGroup>
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuItem>Profile</DropdownMenuItem>
          <DropdownMenuItem>Billing</DropdownMenuItem>
          <DropdownMenuItem>Settings</DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem>GitHub</DropdownMenuItem>
        <DropdownMenuItem>Support</DropdownMenuItem>
        <DropdownMenuItem disabled>API</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Nest secondary actions in a DropdownMenuSub.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function DropdownMenuSubmenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem>Team</DropdownMenuItem>
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            <DropdownMenuItem>Email</DropdownMenuItem>
            <DropdownMenuItem>Message</DropdownMenuItem>
            <DropdownMenuSub>
              <DropdownMenuSubTrigger>More options</DropdownMenuSubTrigger>
              <DropdownMenuSubContent>
                <DropdownMenuItem>Calendly</DropdownMenuItem>
                <DropdownMenuItem>Slack</DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem>Webhook</DropdownMenuItem>
              </DropdownMenuSubContent>
            </DropdownMenuSub>
            <DropdownMenuSeparator />
            <DropdownMenuItem>Advanced...</DropdownMenuItem>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
        <DropdownMenuItem>New Team</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Shortcuts

Use DropdownMenuShortcut to show a key combination at the trailing edge of an item. It displays the key only. Wire the binding yourself.

import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

const styles = stylex.create({
  menu: { minWidth: "10rem" },
})

export default function DropdownMenuShortcuts() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent xstyle={styles.menu}>
        <DropdownMenuGroup>
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuItem>
            Profile
            <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Billing
            <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Settings
            <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          Log out
          <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Icons

Place an icon before the label. The item sizes it.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { CreditCard, LogOut, Settings, User } from "lucide-react"

export default function DropdownMenuIcons() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem>
          <User />
          Profile
        </DropdownMenuItem>
        <DropdownMenuItem>
          <CreditCard />
          Billing
        </DropdownMenuItem>
        <DropdownMenuItem>
          <Settings />
          Settings
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <LogOut />
          Log out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Checkboxes

Use DropdownMenuCheckboxItem for items that toggle. Pass checked and onCheckedChange to control it, or defaultChecked to leave it uncontrolled.

import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

const styles = stylex.create({
  menu: { minWidth: "10rem" },
})

export default function DropdownMenuCheckboxes() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent xstyle={styles.menu}>
        <DropdownMenuGroup>
          <DropdownMenuLabel>Appearance</DropdownMenuLabel>
          <DropdownMenuCheckboxItem defaultChecked>Status Bar</DropdownMenuCheckboxItem>
          <DropdownMenuCheckboxItem disabled>Activity Bar</DropdownMenuCheckboxItem>
          <DropdownMenuCheckboxItem>Panel</DropdownMenuCheckboxItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Radio group

Use DropdownMenuRadioGroup to hold one value across its DropdownMenuRadioItems. Pass value and onValueChange, or defaultValue.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function DropdownMenuRadioGroupDemo() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Open</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuGroup>
          <DropdownMenuLabel>Panel Position</DropdownMenuLabel>
          <DropdownMenuRadioGroup defaultValue="bottom">
            <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
          </DropdownMenuRadioGroup>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Destructive

Set variant="destructive" on an item to colour it and its icon.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Pencil, Share, Trash } from "lucide-react"

export default function DropdownMenuDestructive() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>Actions</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <Pencil />
            Edit
          </DropdownMenuItem>
          <DropdownMenuItem>
            <Share />
            Share
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <Trash />
          Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

API reference

Pass render to replace a part's element or compose it with another component.

Every part wraps the Base UI Menu part of the same name and accepts its props. See the Base UI reference for the full list. Each styled part also takes xstyle and className. The items and DropdownMenuSubTrigger also take style, which is real inline CSS merged over the style their Base UI state resolves to.

PropTypeDefault
side"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""bottom"
sideOffsetnumber4
align"start" | "center" | "end""start"
alignOffsetnumber0

The popup is at least as wide as its trigger and never narrower than 8rem. Set a wider floor through xstyle.

The same props, with side="right", sideOffset={0} and alignOffset={-3} as defaults.

PropTypeDefault
variant"default" | "destructive""default"
insetboolean—
disabledboolean—

inset pads the leading edge by the width of an indicator column, so a plain item lines up with checkbox and radio items beside it. DropdownMenuLabel, DropdownMenuSubTrigger, DropdownMenuCheckboxItem and DropdownMenuRadioItem take inset too.

An item that navigates. It wraps Base UI's Menu.LinkItem and renders an <a>, styled like DropdownMenuItem. Use it for links instead of giving an item render={<a />}.

PropTypeDefault
hrefstring—
closeOnClickbooleanfalse
insetboolean—

The menu stays open on click, since a navigation usually unmounts it. Set closeOnClick on a link that opens a new tab or another app.

PropTypeDefault
checkedboolean—
defaultCheckedboolean—
onCheckedChange(checked: boolean) => void—
PropTypeDefault
valuestring—
defaultValuestring—
onValueChange(value: string) => void—

Data attributes

AttributeOnEffect
data-slot="dropdown-menu-content"the popupPopup surface
data-side, data-alignthe popupBase UI writes them. data-side picks the slide direction
data-starting-style, data-ending-stylethe popupBase UI writes them while the popup animates in or out
data-slot="dropdown-menu-item"an itemTargets items from a parent
data-slot="dropdown-menu-link-item"a linkTargets link items from a parent
data-variantan itemReflects the variant prop
data-insetan itemPresent when inset is set
data-highlighted, data-disabledan itemBase UI writes them