HextaUI

Dropdown menu

A menu of actions and options behind a button, with groups, submenus, checkbox and radio items, and shortcuts.

import {
  IconBrandGithub,
  IconCreditCard,
  IconExternalLink,
  IconKeyboard,
  IconLogout,
  IconMail,
  IconMessage,
  IconPlus,
  IconSettings,
  IconUser,
  IconUserPlus,
} from "@tabler/icons-react"

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

export function DropdownMenuDemo() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>
        Open
      </DropdownMenuTrigger>
      <DropdownMenuContent className="w-56">
        <DropdownMenuGroup>
          <DropdownMenuLabel>My account</DropdownMenuLabel>
          <DropdownMenuItem>
            <IconUser />
            Profile
            <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            <IconCreditCard />
            Billing
            <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            <IconSettings />
            Settings
            <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            <IconKeyboard />
            Keyboard shortcuts
            <DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <IconUserPlus />
              Invite users
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent>
              <DropdownMenuItem>
                <IconMail />
                Email
              </DropdownMenuItem>
              <DropdownMenuItem>
                <IconMessage />
                Message
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem>
                <IconPlus />
                More…
              </DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuItem
            render={<a href="https://github.com" target="_blank" />}
          >
            <IconBrandGithub />
            GitHub
            <IconExternalLink className="ms-auto" />
          </DropdownMenuItem>
          <DropdownMenuItem disabled>API</DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <IconLogout />
          Log out
          <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/dropdown-menu.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
<DropdownMenu>
  <DropdownMenuTrigger render={<Button variant="outline" />}>
    Open
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem>Profile</DropdownMenuItem>
    <DropdownMenuItem>Billing</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
DropdownMenu
├── DropdownMenuTrigger
└── DropdownMenuContent
    ├── DropdownMenuGroup
    │   ├── DropdownMenuLabel
    │   └── DropdownMenuItem
    │       └── DropdownMenuShortcut
    ├── DropdownMenuCheckboxItem
    ├── DropdownMenuRadioGroup
    │   └── DropdownMenuRadioItem
    ├── DropdownMenuSeparator
    └── DropdownMenuSub
        ├── DropdownMenuSubTrigger
        └── DropdownMenuSubContent

Safe area

Moving diagonally from a submenu trigger toward its submenu crosses sibling triggers. The safe area keeps the submenu open while the pointer heads into it. Turn on showSafeArea to see it live: the dot turns green inside the area and red where the submenu would switch.

"use client"

import * as React from "react"
import { IconDots } from "@tabler/icons-react"

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

const folders = [
  "Inbox",
  "Design",
  "Engineering",
  "Marketing",
  "Finance",
  "Legal",
  "Archive",
  "Shared with me",
]

const submenus = [
  { label: "Move to", items: folders },
  { label: "Copy to", items: folders },
  { label: "Share", items: ["Copy link", "Email", "Slack"] },
  { label: "Export as", items: ["PDF", "PNG", "SVG"] },
]

export function DropdownMenuSafeArea() {
  const [showSafeArea, setShowSafeArea] = React.useState(true)

  return (
    <div className="flex flex-col items-center gap-6">
      <label className="flex items-center gap-3 text-sm">
        <Checkbox
          checked={showSafeArea}
          onCheckedChange={(checked) => setShowSafeArea(checked === true)}
        />
        Show safe area
      </label>
      <DropdownMenu showSafeArea={showSafeArea}>
        <DropdownMenuTrigger render={<Button variant="outline" />}>
          <IconDots data-icon="inline-start" />
          Q3 report.pdf
        </DropdownMenuTrigger>
        <DropdownMenuContent className="w-44">
          {submenus.map((submenu) => (
            <DropdownMenuSub key={submenu.label}>
              <DropdownMenuSubTrigger>{submenu.label}</DropdownMenuSubTrigger>
              <DropdownMenuSubContent className="w-44">
                {submenu.items.map((item) => (
                  <DropdownMenuItem key={item}>{item}</DropdownMenuItem>
                ))}
              </DropdownMenuSubContent>
            </DropdownMenuSub>
          ))}
          <DropdownMenuSeparator />
          <DropdownMenuItem>Rename</DropdownMenuItem>
          <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}

Checkboxes

<DropdownMenuCheckboxItem /> toggles a setting and keeps the menu open, so several can be changed in a row. The check draws in and out instead of popping.

"use client"

import * as React from "react"
import { IconChevronDown } from "@tabler/icons-react"

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

export function DropdownMenuCheckboxes() {
  const [statusBar, setStatusBar] = React.useState(true)
  const [activityBar, setActivityBar] = React.useState(false)

  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>
        View
        <IconChevronDown data-icon="inline-end" />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuGroup>
          <DropdownMenuLabel>Appearance</DropdownMenuLabel>
          <DropdownMenuCheckboxItem
            checked={statusBar}
            onCheckedChange={setStatusBar}
          >
            Status bar
          </DropdownMenuCheckboxItem>
          <DropdownMenuCheckboxItem
            checked={activityBar}
            onCheckedChange={setActivityBar}
          >
            Activity bar
          </DropdownMenuCheckboxItem>
          <DropdownMenuCheckboxItem disabled>Panel</DropdownMenuCheckboxItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Radio group

Wrap <DropdownMenuRadioItem /> in a <DropdownMenuRadioGroup /> to pick one option. A label inside the group names it for screen readers.

"use client"

import * as React from "react"
import { IconChevronDown } from "@tabler/icons-react"

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

export function DropdownMenuRadioGroupDemo() {
  const [position, setPosition] = React.useState("bottom")

  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>
        Panel position
        <IconChevronDown data-icon="inline-end" />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
          <DropdownMenuLabel>Panel position</DropdownMenuLabel>
          <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Select-like

The menu is never narrower than its trigger. Set closeOnClick on radio items when picking one should also close the menu.

"use client"

import * as React from "react"
import { IconChevronDown, IconPlus } from "@tabler/icons-react"

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

const workspaces = [
  "Acme Inc.",
  "Monsters Inc.",
  "A workspace with a very long name that wraps",
]

export function DropdownMenuSelect() {
  const [workspace, setWorkspace] = React.useState(workspaces[0])

  return (
    <div className="w-64 max-w-full">
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <Button variant="outline" className="w-full justify-between" />
          }
        >
          <span className="truncate">{workspace}</span>
          <IconChevronDown data-icon="inline-end" />
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuRadioGroup
            value={workspace}
            onValueChange={setWorkspace}
          >
            {workspaces.map((name) => (
              <DropdownMenuRadioItem key={name} value={name} closeOnClick>
                {name}
              </DropdownMenuRadioItem>
            ))}
          </DropdownMenuRadioGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <IconPlus />
            New workspace
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}

Row actions

One menu serves every row. Create a handle with createDropdownMenuHandle, pass it to each trigger with a payload, and read the payload in the menu. Delete hands off to an alert dialog.

  • Quarterly report.pdf
  • Brand assets
  • Roadmap 2027.md
"use client"

import * as React from "react"
import { IconDots, IconPencil, IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  createDropdownMenuHandle,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

const files = ["Quarterly report.pdf", "Brand assets", "Roadmap 2027.md"]

const rowMenu = createDropdownMenuHandle<{ name: string }>()

export function DropdownMenuRowActions() {
  const [pending, setPending] = React.useState<string | null>(null)
  const [removed, setRemoved] = React.useState<string[]>([])

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <ul className="flex flex-col gap-0.5 rounded-xl border p-1">
        {files
          .filter((name) => !removed.includes(name))
          .map((name) => (
            <li
              key={name}
              className="flex items-center gap-3 rounded-lg py-1 ps-3 pe-1 text-sm"
            >
              <span className="min-w-0 flex-1 truncate">{name}</span>
              <DropdownMenuTrigger
                handle={rowMenu}
                payload={{ name }}
                aria-label={`Actions for ${name}`}
                render={<Button variant="ghost" size="icon-sm" />}
              >
                <IconDots />
              </DropdownMenuTrigger>
            </li>
          ))}
      </ul>
      <div>
        <Button variant="outline" size="sm" onClick={() => setRemoved([])}>
          Restore files
        </Button>
      </div>
      <DropdownMenu handle={rowMenu}>
        {({ payload }) => (
          <DropdownMenuContent align="end">
            <DropdownMenuItem>
              <IconPencil />
              Rename
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem
              variant="destructive"
              onClick={() => setPending(payload?.name ?? null)}
            >
              <IconTrash />
              Delete…
            </DropdownMenuItem>
          </DropdownMenuContent>
        )}
      </DropdownMenu>
      <AlertDialog
        open={pending !== null}
        onOpenChange={(open) => {
          if (!open) {
            setPending(null)
          }
        }}
      >
        <AlertDialogContent size="sm">
          <AlertDialogHeader>
            <AlertDialogTitle>Delete “{pending}”?</AlertDialogTitle>
            <AlertDialogDescription>
              It moves to the trash for 30 days.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              onClick={() => {
                if (pending) {
                  setRemoved((value) => [...value, pending])
                }
                setPending(null)
              }}
            >
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}

Controlled

Pass open and onOpenChange. The second argument says why the menu changed, such as a trigger press, an item press or Escape. Items with closeOnClick={false} keep it open.

Open: false · last reason: none

"use client"

import * as React from "react"

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

export function DropdownMenuControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("none")

  return (
    <div className="flex flex-col items-center gap-3">
      <DropdownMenu
        open={open}
        onOpenChange={(next, details) => {
          setOpen(next)
          setReason(details.reason)
        }}
      >
        <DropdownMenuTrigger render={<Button variant="outline" />}>
          Controlled
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Closes the menu</DropdownMenuItem>
          <DropdownMenuItem closeOnClick={false}>Stays open</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · last reason: {reason}
      </p>
    </div>
  )
}

Open on hover

openOnHover on the trigger opens the menu after delay and closes it after closeDelay. Clicking and the keyboard still work, so touch and keyboard users aren’t locked out.

import { IconChevronDown } from "@tabler/icons-react"

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

export function DropdownMenuHover() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        openOnHover
        delay={120}
        closeDelay={120}
        render={<Button variant="ghost" />}
      >
        Products
        <IconChevronDown data-icon="inline-end" />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem>Analytics</DropdownMenuItem>
        <DropdownMenuItem>Engagement</DropdownMenuItem>
        <DropdownMenuItem>Security</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Disabled

A disabled trigger never opens. Disabled items stay visible and are skipped by the arrow keys.

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

export function DropdownMenuDisabled() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <DropdownMenu>
        <DropdownMenuTrigger disabled render={<Button variant="outline" />}>
          Disabled trigger
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Never shown</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="outline" />}>
          Disabled items
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Rename</DropdownMenuItem>
          <DropdownMenuItem disabled>Move (no permission)</DropdownMenuItem>
          <DropdownMenuItem disabled>Share (no permission)</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}

Long content

Labels wrap instead of stretching the menu past 20rem, unbroken strings break anywhere, and shortcuts stay on the first line.

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

export function DropdownMenuLongContent() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>
        Long labels
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuLabel>
          A label that goes on for much longer than any menu label should
        </DropdownMenuLabel>
        <DropdownMenuItem>
          Move to “Supercalifragilisticexpialidocious archive 2026”
          <DropdownMenuShortcut>⌥⌘M</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuItem>
          https://example.com/a/really/long/url/without/any/spaces/at/all
        </DropdownMenuItem>
        <DropdownMenuItem>👩‍👩‍👧‍👦 日本語のテキスト</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Scrolling

When there isn’t room for every item, the menu scrolls inside the viewport and the highlighted item scrolls into view.

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

export function DropdownMenuScrolling() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" />}>
        40 documents
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        {Array.from({ length: 40 }, (_, index) => (
          <DropdownMenuItem key={index}>Document {index + 1}</DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Inside a sheet

The menu layers above the sheet. Escape closes the menu first, then the sheet.

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function DropdownMenuInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Sheet</SheetTitle>
          <SheetDescription>
            The menu layers above the sheet, and Escape closes only the menu.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <DropdownMenu>
            <DropdownMenuTrigger render={<Button variant="outline" />}>
              Open menu
            </DropdownMenuTrigger>
            <DropdownMenuContent>
              <DropdownMenuItem>Copy</DropdownMenuItem>
              <DropdownMenuSub>
                <DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
                <DropdownMenuSubContent>
                  <DropdownMenuItem>Email</DropdownMenuItem>
                  <DropdownMenuItem>Link</DropdownMenuItem>
                </DropdownMenuSubContent>
              </DropdownMenuSub>
            </DropdownMenuContent>
          </DropdownMenu>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

Right to left

The menu, submenu chevron and submenu side follow the trigger’s direction. Shortcuts keep their left-to-right order.

import {
  IconChevronDown,
  IconLogout,
  IconUser,
  IconUserPlus,
} from "@tabler/icons-react"

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

export function DropdownMenuRtl() {
  return (
    <div dir="rtl">
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="outline" />}>
          الحساب
          <IconChevronDown data-icon="inline-end" />
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>
            <IconUser />
            الملف الشخصي
            <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuCheckboxItem defaultChecked>
            إظهار الشريط
          </DropdownMenuCheckboxItem>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <IconUserPlus />
              دعوة
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent>
              <DropdownMenuItem>بريد</DropdownMenuItem>
              <DropdownMenuItem>رسالة</DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSeparator />
          <DropdownMenuItem variant="destructive">
            <IconLogout />
            تسجيل الخروج
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}
KeyAction
EnterSpace↓On the trigger, opens the menu and highlights the first item.
↓Highlights the next item, wrapping at the end.
↑Highlights the previous item, wrapping at the start.
HomeHighlights the first item.
EndHighlights the last item.
EnterSpaceRuns the highlighted item. Checkbox and radio items toggle and keep the menu open.
→Opens the highlighted submenu and moves into it. ← in right-to-left layouts.
←Closes the current submenu and returns to its trigger. → in right-to-left layouts.
EscCloses the current menu and returns focus to its trigger. In a submenu, only that submenu closes.
A–ZHighlights the next item starting with that letter.
  • The trigger is announced as a menu button, and focus returns to it when the menu closes.
  • Give icon-only triggers an aria-label, like the row actions example does.
  • <DropdownMenuShortcut /> is a visual label only. Bind the keys yourself.
  • With reduced motion on, the menu fades without scaling and the item blink is skipped.

Built on the Base UI menu. Every part accepts the props of the primitive it wraps.

PropTypeDefault
open
boolean–
defaultOpen
booleanfalse
showSafeAreaDraws the submenu safe area while the menu is open. For debugging and demos.
booleanfalse
onOpenChangedetails.reason says what caused the change.
(open: boolean, details) => void–
modalLocks page scroll and blocks outside clicks while open.
booleantrue
loopFocusWrap the arrow keys at either end.
booleantrue
handleConnects detached triggers to this menu.
DropdownMenuHandle<Payload>–
childrenUse the function form to read the opening trigger’s payload.
ReactNode | ({ payload }) => ReactNode–
disabled
booleanfalse
PropTypeDefault
openOnHover
booleanfalse
delayMilliseconds before a hover opens the menu.
number100
closeDelayMilliseconds before a hover-opened menu closes.
number0
handle
DropdownMenuHandle<Payload>–
payloadPassed to the menu when this trigger opens it.
Payload–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-slot="dropdown-menu-trigger"Target the trigger in CSS.
data-popup-openPresent while its menu is open.
data-pressedPresent while the trigger is pressed.
data-disabledPresent when the trigger is disabled.
PropTypeDefault
side
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number4
alignOffset
number0
collisionPaddingSpace kept between the menu and the edge of the viewport.
number | Rect8
anchorPosition against another element instead of the trigger.
Element | RefObject | VirtualElement–
sticky
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="dropdown-menu-content"Target the menu in CSS.
data-openPresent while the menu is open.
data-starting-stylePresent while the menu animates in.
data-ending-stylePresent while the menu animates out.
data-sideThe side the menu settled on after collision handling.
data-alignThe alignment it settled on.
data-chosenPresent after an item is clicked, so the menu waits for the item’s confirmation blink before closing.
--anchor-widthThe trigger’s width. The menu is at least this wide.
--available-heightRoom left in the viewport. Long menus scroll within it.
--transform-originWhere the scale animation grows from, next to the trigger.
PropTypeDefault
variant
"default" | "destructive""default"
insetIndents the item to line up with checkbox items.
booleanfalse
closeOnClick
booleantrue
onClick
(event) => void–
labelText used for type-ahead when children aren’t plain text.
string–
disabled
booleanfalse
renderRender a link with render={<a href="…" />}.
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="dropdown-menu-item"Target items in CSS.
data-variantThe current variant.
data-highlightedPresent while the item is highlighted by pointer or keyboard.
data-disabledPresent when the item is disabled.
data-chosenPresent on the item that was just clicked, while it blinks.
data-insetPresent when inset is set.
PropTypeDefault
checked
boolean–
defaultChecked
booleanfalse
onCheckedChange
(checked: boolean, details) => void–
closeOnClick
booleanfalse
inset
booleanfalse
disabled
booleanfalse
AttributeDescription
data-slot="dropdown-menu-checkbox-item"Target checkbox items in CSS.
data-checkedPresent when checked.
data-uncheckedPresent when not checked.
data-highlightedPresent while the item is highlighted by pointer or keyboard.
data-disabledPresent when the item is disabled.
data-chosenPresent on the item that was just clicked, while it blinks.
data-insetPresent when inset is set.
PropTypeDefault
value
Value–
defaultValue
Value–
onValueChange
(value: Value, details) => void–
disabled
booleanfalse
PropTypeDefault
value
Value–
closeOnClick
booleanfalse
inset
booleanfalse
disabled
booleanfalse
AttributeDescription
data-slot="dropdown-menu-radio-item"Target radio items in CSS.
data-checkedPresent on the selected item.
data-uncheckedPresent on the other items.
data-highlightedPresent while the item is highlighted by pointer or keyboard.
data-disabledPresent when the item is disabled.
data-chosenPresent on the item that was just clicked, while it blinks.
data-insetPresent when inset is set.

Groups related items. A <DropdownMenuLabel /> inside it becomes the group’s accessible name.

PropTypeDefault
inset
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>

A divider between groups, announced as a separator.

PropTypeDefault
dirShortcuts keep their key order in right-to-left menus.
"ltr" | "rtl""ltr"
PropTypeDefault
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean, details) => void–
closeParentOnEscClose the whole menu, not just this submenu, on Escape.
booleanfalse
PropTypeDefault
inset
booleanfalse
disabled
booleanfalse
delayMilliseconds before hovering opens the submenu.
number100
closeDelay
number0
AttributeDescription
data-slot="dropdown-menu-sub-trigger"Target submenu triggers in CSS.
data-popup-openPresent while its submenu is open.
data-highlightedPresent while highlighted.
PropTypeDefault
sideOffset
number0
alignOffset
number-4
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="dropdown-menu-sub-content"Target submenus in CSS.
data-openPresent while the menu is open.
data-starting-stylePresent while the menu animates in.
data-ending-stylePresent while the menu animates out.
data-sideThe side the menu settled on after collision handling.
data-alignThe alignment it settled on.
data-chosenPresent after an item is clicked, so the menu waits for the item’s confirmation blink before closing.
--available-heightRoom left in the viewport. Long menus scroll within it.
--transform-originWhere the scale animation grows from, next to the trigger.