Skip to content

Dropdown menu

A menu of actions or options that opens from a button.

Status
Stable
Category
Overlays
Adoption
Not used yet
import { DropdownMenu } from "@oration/canon/components/dropdown-menu";
packages/canon/src/components/dropdown-menu.tsx

INV-20931

Northwind Freight, $18,240.00, owned by Priya Raman

import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuRadioGroup,  DropdownMenuRadioItem,  DropdownMenuSeparator,  DropdownMenuShortcut,  DropdownMenuSub,  DropdownMenuSubContent,  DropdownMenuSubTrigger,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { Tooltip, TooltipContent, TooltipTrigger } from "@oration/canon/components/tooltip";import {  CopyIcon,  DownloadIcon,  ExternalLinkIcon,  LinkIcon,  MoreHorizontalIcon,  Trash2Icon,  UserIcon,} from "lucide-react";import Link from "next/link";import * as React from "react";export function Hero() {    const [owner, setOwner] = React.useState("Priya Raman");    return (        <div className="flex w-full max-w-xl items-center gap-3 rounded-xl bg-card px-4 py-3 text-left shadow-border">            <div className="min-w-0 flex-1">                <p className="truncate text-13 font-medium">INV-20931</p>                <p className="truncate text-xs text-muted-foreground">                    Northwind Freight, $18,240.00, owned by {owner}                </p>            </div>            <DropdownMenu>                <Tooltip>                    <TooltipTrigger                        render={                            <DropdownMenuTrigger                                render={                                    <Button                                        type="button"                                        variant="ghost"                                        size="icon-sm"                                        aria-label="More actions for INV-20931"                                    />                                }                            />                        }                    >                        <MoreHorizontalIcon aria-hidden="true" />                    </TooltipTrigger>                    <TooltipContent>More actions</TooltipContent>                </Tooltip>                <DropdownMenuContent align="end" className="w-56">                    <DropdownMenuItem                        onClick={() => toast.add({ title: "Opened INV-20931" })}                    >                        <ExternalLinkIcon aria-hidden="true" />                        Open invoice                        <DropdownMenuShortcut>↵</DropdownMenuShortcut>                    </DropdownMenuItem>                    <DropdownMenuItem                        onClick={() =>                            toast.add({                                title: "Invoice duplicated",                                description:                                    "INV-20932 is a draft for Northwind Freight.",                            })                        }                    >                        <CopyIcon aria-hidden="true" />                        Duplicate                        <DropdownMenuShortcut>⌘D</DropdownMenuShortcut>                    </DropdownMenuItem>                    <DropdownMenuItem                        onClick={() =>                            toast.add({ title: "Downloading INV-20931.pdf" })                        }                    >                        <DownloadIcon aria-hidden="true" />                        Download PDF                    </DropdownMenuItem>                    <DropdownMenuItem                        onClick={() => toast.add({ title: "Link copied" })}                    >                        <LinkIcon aria-hidden="true" />                        Copy link                    </DropdownMenuItem>                    <DropdownMenuSeparator />                    <DropdownMenuSub>                        <DropdownMenuSubTrigger>                            <UserIcon aria-hidden="true" />                            Change owner                        </DropdownMenuSubTrigger>                        <DropdownMenuSubContent className="w-48">                            <DropdownMenuRadioGroup                                value={owner}                                onValueChange={(value) => {                                    setOwner(String(value));                                    toast.add({                                        title: `Owner changed to ${value}`,                                    });                                }}                            >                                {[                                    "Priya Raman",                                    "Jordan Lee",                                    "Aisha Bello",                                ].map((name) => (                                    <DropdownMenuRadioItem                                        key={name}                                        value={name}                                    >                                        {name}                                    </DropdownMenuRadioItem>                                ))}                            </DropdownMenuRadioGroup>                        </DropdownMenuSubContent>                    </DropdownMenuSub>                    <DropdownMenuSeparator />                    <DropdownMenuItem                        variant="destructive"                        onClick={() =>                            toast.add({                                type: "error",                                title: "INV-20931 voided",                                description:                                    "It won't be paid in Friday's run.",                            })                        }                    >                        <Trash2Icon aria-hidden="true" />                        Void invoice                    </DropdownMenuItem>                </DropdownMenuContent>            </DropdownMenu>        </div>    );}

Usage#

Dropdown menu is a list of actions or options that opens from a button: a row's More actions, a toolbar's Sort, the user menu. Items can carry an icon and a shortcut, toggle as checkboxes, pick one value as a radio group, open a submenu, or remove something in Signal Red. It is the most common overlay in the product. The two things people get wrong are using it for navigation that belongs in the page, and hiding a view's main action inside it. Menus hold the secondary actions; the primary one stays a button.

When to use

  • For a row's or record's secondary actions behind a More actions icon button: duplicate, download, copy link, delete.
  • For view options that are a choice from a short list: Sort by, Group by, which columns show.
  • For the user and workspace menus, with a label, groups and a theme submenu.
  • For a split button's alternates, such as Save with Save as template.

When not to use

  • To choose a value for a form field. A select shows the chosen value and belongs to the form. Use Select
  • For controls such as switches, inputs or a segmented control. Use Popover
  • For searching or running any command from the keyboard. Use Command menu
  • For the same actions on right-click. Mirror this menu in a context menu instead of replacing it. Use Context menu
  • For the one action the view exists for. Keep it a visible button. Use Button

Destructive is a tint

A destructive item is Signal Red text that highlights with a 10% red tint (20% in dark), and it sits last, after a separator. It never becomes a solid red row.

The Hairline-and-Lift Rule

The menu takes its edge from the overlay shadow and a 1px ink ring at 10%. Separators are the only lines inside it.

Anatomy#

INV-20931

Duplicate⌘D
Change owner
Void invoice
  1. Trigger. A button, often a ghost icon-sm with MoreHorizontalIcon and an aria-label that names the object. It keeps its hover fill while open.
  2. Surface. Popover White, 10px corners, 4px padding, the overlay shadow and ring. At least 8rem wide, and as wide as the trigger unless you set a width.
  3. Group label. 12px medium Slate Meta, for naming a group or the object.
  4. Item. A 28px row with 8px corners, a 16px icon and 14px text, highlighted in Menu Hover.
  5. Shortcut. 12px Slate Meta at the end of the row.
  6. Submenu trigger. An item with a trailing chevron. It stays highlighted while its submenu is open.
  7. Separator. A Hairline that runs to the menu's edges, 4px above and below.
  8. Destructive item. Signal Red text and icon, last in the menu.

Examples#

Icons and shortcuts

Icons are 16px and sit before the label; DropdownMenuShortcut pushes the shortcut to the end. Items that navigate render a Link through render.

import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuSeparator,  DropdownMenuShortcut,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { ChevronDownIcon, DownloadIcon, FileTextIcon, SettingsIcon } from "lucide-react";import Link from "next/link";export function IconsAndShortcuts() {    return (        <DropdownMenu>            <DropdownMenuTrigger                render={<Button type="button" variant="outline" size="sm" />}            >                <DownloadIcon data-icon="inline-start" aria-hidden="true" />                Export                <ChevronDownIcon data-icon="inline-end" aria-hidden="true" />            </DropdownMenuTrigger>            <DropdownMenuContent className="w-56">                <DropdownMenuItem                    onClick={() =>                        toast.add({                            title: "Exporting 212 invoices",                            description: "invoices-sep-2026.csv",                        })                    }                >                    <FileTextIcon aria-hidden="true" />                    Export as CSV                    <DropdownMenuShortcut>⇧⌘E</DropdownMenuShortcut>                </DropdownMenuItem>                <DropdownMenuItem                    onClick={() =>                        toast.add({                            title: "Exporting 212 invoices",                            description: "invoices-sep-2026.pdf",                        })                    }                >                    <DownloadIcon aria-hidden="true" />                    Export as PDF                </DropdownMenuItem>                <DropdownMenuSeparator />                <DropdownMenuItem render={<Link href="/settings" />}>                    <SettingsIcon aria-hidden="true" />                    Export settings                </DropdownMenuItem>            </DropdownMenuContent>        </DropdownMenu>    );}

Checkbox items

For independent toggles such as visible columns. The menu stays open while you change several, and each checked item shows a check at the right.

import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuCheckboxItem,  DropdownMenuContent,  DropdownMenuGroup,  DropdownMenuLabel,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { Columns3Icon } from "lucide-react";import * as React from "react";export function CheckboxItems() {    const [columns, setColumns] = React.useState({        due: true,        terms: true,        owner: false,        remittance: false,    });    const labels = {        due: "Due date",        terms: "Terms",        owner: "Owner",        remittance: "Remittance",    };    const shown = Object.values(columns).filter(Boolean).length;    return (        <DropdownMenu>            <DropdownMenuTrigger                render={<Button type="button" variant="ghost" size="sm" />}            >                <Columns3Icon data-icon="inline-start" aria-hidden="true" />                Columns                <span className="text-xs text-muted-foreground tabular-nums">                    {shown + 2}                </span>            </DropdownMenuTrigger>            <DropdownMenuContent className="w-48">                <DropdownMenuGroup>                    <DropdownMenuLabel>Show columns</DropdownMenuLabel>                    {(Object.keys(labels) as (keyof typeof labels)[]).map(                        (key) => (                            <DropdownMenuCheckboxItem                                key={key}                                checked={columns[key]}                                onCheckedChange={(checked) =>                                    setColumns((current) => ({                                        ...current,                                        [key]: checked,                                    }))                                }                            >                                {labels[key]}                            </DropdownMenuCheckboxItem>                        ),                    )}                </DropdownMenuGroup>            </DropdownMenuContent>        </DropdownMenu>    );}

Radio group

For one choice from a short list, such as the sort order. The trigger says the current value. Pass closeOnClick when choosing should close the menu.

import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuGroup,  DropdownMenuLabel,  DropdownMenuRadioGroup,  DropdownMenuRadioItem,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { ArrowDownUpIcon } from "lucide-react";import * as React from "react";export function RadioGroup() {    const options = {        due: "Due date",        amount: "Amount",        supplier: "Supplier name",        received: "Date received",    };    const [sort, setSort] = React.useState<keyof typeof options>("due");    return (        <DropdownMenu>            <DropdownMenuTrigger                render={<Button type="button" variant="ghost" size="sm" />}            >                <ArrowDownUpIcon data-icon="inline-start" aria-hidden="true" />                Sort: {options[sort]}            </DropdownMenuTrigger>            <DropdownMenuContent className="w-48">                <DropdownMenuGroup>                    <DropdownMenuLabel>Sort by</DropdownMenuLabel>                    <DropdownMenuRadioGroup                        value={sort}                        onValueChange={(value) =>                            setSort(value as keyof typeof options)                        }                    >                        {(Object.keys(options) as (keyof typeof options)[]).map(                            (key) => (                                <DropdownMenuRadioItem                                    key={key}                                    value={key}                                    closeOnClick                                >                                    {options[key]}                                </DropdownMenuRadioItem>                            ),                        )}                    </DropdownMenuRadioGroup>                </DropdownMenuGroup>            </DropdownMenuContent>        </DropdownMenu>    );}

Labels, groups and separators

The account menu: a label naming who is signed in, groups of related items split by hairlines, and a labelled radio group for the theme.

import { Avatar, AvatarFallback } from "@oration/canon/components/avatar";import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuGroup,  DropdownMenuItem,  DropdownMenuLabel,  DropdownMenuRadioGroup,  DropdownMenuRadioItem,  DropdownMenuSeparator,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { LogOutIcon, UserIcon, UserPlusIcon } from "lucide-react";import Link from "next/link";import * as React from "react";export function LabelsAndGroups() {    const [theme, setTheme] = React.useState("system");    return (        <DropdownMenu>            <DropdownMenuTrigger                render={                    <Button                        type="button"                        variant="ghost"                        className="h-10 gap-2 px-2"                        aria-label="Account menu for Maya Okafor"                    />                }            >                <Avatar size="sm">                    <AvatarFallback>MO</AvatarFallback>                </Avatar>                <span className="text-13">Maya Okafor</span>            </DropdownMenuTrigger>            <DropdownMenuContent className="w-60">                <DropdownMenuGroup>                    <DropdownMenuLabel className="flex flex-col font-normal">                        <span className="text-13 font-medium text-foreground">                            Maya Okafor                        </span>                        <span>maya@cedarline.com</span>                    </DropdownMenuLabel>                </DropdownMenuGroup>                <DropdownMenuSeparator />                <DropdownMenuGroup>                    <DropdownMenuItem render={<Link href="/settings" />}>                        <UserIcon aria-hidden="true" />                        Profile                    </DropdownMenuItem>                    <DropdownMenuItem                        onClick={() =>                            toast.add({                                title: "Invite link copied",                                description:                                    "Anyone with the link can join Cedarline.",                            })                        }                    >                        <UserPlusIcon aria-hidden="true" />                        Invite teammates                    </DropdownMenuItem>                </DropdownMenuGroup>                <DropdownMenuSeparator />                <DropdownMenuGroup>                    <DropdownMenuLabel>Theme</DropdownMenuLabel>                    <DropdownMenuRadioGroup                        value={theme}                        onValueChange={(value) => setTheme(String(value))}                    >                        <DropdownMenuRadioItem value="light">                            Light                        </DropdownMenuRadioItem>                        <DropdownMenuRadioItem value="dark">                            Dark                        </DropdownMenuRadioItem>                        <DropdownMenuRadioItem value="system">                            System                        </DropdownMenuRadioItem>                    </DropdownMenuRadioGroup>                </DropdownMenuGroup>                <DropdownMenuSeparator />                <DropdownMenuItem                    onClick={() =>                        toast.add({ title: "Signed out of Cedarline" })                    }                >                    <LogOutIcon aria-hidden="true" />                    Sign out                </DropdownMenuItem>            </DropdownMenuContent>        </DropdownMenu>    );}

Destructive and disabled items

The destructive item is last, after a separator, in Signal Red with a 10% tint on highlight. Suspend Jordan Lee and Reset two-factor becomes disabled.

import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuLabel,  DropdownMenuSeparator,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { MoreHorizontalIcon, Trash2Icon } from "lucide-react";import * as React from "react";export function DestructiveAndDisabled() {    const [suspended, setSuspended] = React.useState(false);    return (        <DropdownMenu>            <DropdownMenuTrigger                render={                    <Button                        type="button"                        variant="ghost"                        size="icon-sm"                        aria-label="More actions for Jordan Lee"                    />                }            >                <MoreHorizontalIcon aria-hidden="true" />            </DropdownMenuTrigger>            <DropdownMenuContent align="end" className="w-56">                <DropdownMenuLabel>Jordan Lee</DropdownMenuLabel>                <DropdownMenuItem                    disabled={suspended}                    onClick={() => toast.add({ title: "Two-factor reset" })}                >                    Reset two-factor                </DropdownMenuItem>                <DropdownMenuItem                    onClick={() => {                        setSuspended((value) => !value);                        toast.add({                            title: suspended                                ? "Jordan Lee reactivated"                                : "Jordan Lee suspended",                        });                    }}                >                    {suspended ? "Reactivate" : "Suspend"}                </DropdownMenuItem>                <DropdownMenuSeparator />                <DropdownMenuItem                    variant="destructive"                    onClick={() =>                        toast.add({                            type: "error",                            title: "Jordan Lee removed",                            description:                                "Their 14 open invoices moved to Maya Okafor.",                        })                    }                >                    <Trash2Icon aria-hidden="true" />                    Remove from Cedarline                </DropdownMenuItem>            </DropdownMenuContent>        </DropdownMenu>    );}

States#

Rest
Duplicate
Highlighted
Duplicate
Checked
Due date
Submenu open
Change owner
Disabled
Reset two-factor
Destructive
Void invoice
Destructive highlighted
Void invoice
import { cn } from "@oration/canon/lib/utils";import { CheckIcon, ChevronRightIcon } from "lucide-react";export function StatesMatrix() {    const item =        "relative flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm [&_svg]:size-4 [&_svg]:shrink-0";    const rows = [        { state: "Rest", className: "", label: "Duplicate", check: false },        {            state: "Highlighted",            className: "bg-accent",            label: "Duplicate",            check: false,        },        { state: "Checked", className: "pr-8", label: "Due date", check: true },        {            state: "Submenu open",            className: "bg-accent",            label: "Change owner",            check: false,        },        {            state: "Disabled",            className: "opacity-50",            label: "Reset two-factor",            check: false,        },        {            state: "Destructive",            className: "text-destructive",            label: "Void invoice",            check: false,        },        {            state: "Destructive highlighted",            className:                "bg-destructive/10 text-destructive dark:bg-destructive/20",            label: "Void invoice",            check: false,        },    ];    return (        <div            className="grid w-full max-w-md grid-cols-[10rem_minmax(0,1fr)] items-center gap-x-4 gap-y-1"            inert        >            {rows.map((row) => (                <div key={row.state} className="contents">                    <span className="text-13 text-muted-foreground">                        {row.state}                    </span>                    <div className={cn(item, row.className)}>                        {row.label}                        {row.state === "Submenu open" ? (                            <ChevronRightIcon                                aria-hidden="true"                                className="ml-auto"                            />                        ) : null}                        {row.check ? (                            <span className="absolute right-2 flex items-center">                                <CheckIcon aria-hidden="true" />                            </span>                        ) : null}                    </div>                </div>            ))}        </div>    );}
States
StateTreatment
ClosedThe trigger has aria-expanded="false".
OpeningGrows from a 0.97 scale and slides 8px from the trigger over 160ms on the house ease-out.
Item restNo fill. Text in Popover ink, icons 16px.
Item highlightedMenu Hover fill from pointer or arrow keys; the highlight follows the pointer (highlightItemOnHover).
Destructive highlightedA 10% Signal Red tint (20% in dark) behind red text.
CheckedCheckbox and radio items show a 16px check at the right edge.
Submenu openThe submenu trigger keeps its Menu Hover fill.
Disabled50% opacity and no pointer events. Arrow keys still reach it, so screen reader users hear that it exists, but it can't be chosen.
ClosingFades out toward a 0.95 scale in 110ms.

Behavior#

  • Built on Base UI Menu. Click, Enter, Space or Down on the trigger opens it. Focus moves into the menu, arrow keys move the highlight and wrap at the ends, and typing jumps to the item that starts with those letters.
  • Choosing an item runs its onClick and closes the menu. Checkbox and radio items stay open by default so several can be changed; set closeOnClick to change either.
  • Escape or a click outside closes it, and focus returns to the trigger.
  • Modal by default: while open, the page doesn't scroll and outside clicks only close the menu. Pass modal={false} for menus in toolbars that shouldn't lock the page.
  • align defaults to start, so the menu lines up with the trigger's left edge; row action menus at the right of a row use align="end". It flips and shifts to stay on screen, and scrolls inside the available height.
  • Submenus open to the right on hover after 100ms, or with Right or Enter, and close with Left or Escape. They reposition to the left near the viewport edge.
  • Menu items that navigate render a Next Link through render, so they keep link behavior.
  • Motion is shared with popovers and selects: 160ms in from 0.97 and 110ms out, a fade only under reduced motion.

Do and don't#

Suspend
Reset two-factor
Remove from Cedarline
Do. Name the trigger for the object, More actions for INV-20931, and put the destructive item last after a separator.
Suspend
Remove from Cedarline
Reset two-factor
Don't. Mix the destructive item into the middle of the list, where a slip of the pointer removes the supplier.
Do. Keep the view's main action as a visible button and use the menu for the rest.
Don't. Hide the only way to approve an invoice inside a menu. People scan for buttons, not kebabs.
Do. Keep menus to about seven items in groups, with a submenu for a long set of values.
Don't. List fifteen items in one column. Past a screen, it's a select, a combobox or a command menu.

Content#

  • Items start with a verb and name the object when it isn't obvious: Download PDF, Copy link, Remove from Cedarline.
  • Use the same verb the rest of the product uses: if the button says Void invoice, the menu does too.
  • Option items are nouns in sentence case: Due date, Amount, Supplier name.
  • An item that opens a dialog ends with an ellipsis only if the dialog asks for more input: Move to payment run….
  • Shortcuts use symbols: ⌘D, ⇧⌘C.

Accessibility#

  • The trigger gets aria-haspopup="menu" and aria-expanded; the surface is role="menu", items are menuitem, menuitemcheckbox or menuitemradio with aria-checked.
  • Icon-only triggers need an aria-label that names the object, such as More actions for INV-20931, and a tooltip.
  • Group labels name their group for screen readers through Menu.GroupLabel; wrap labelled items in a DropdownMenuGroup.
  • Item icons are decorative; the label names the action.
  • Focus returns to the trigger on close, so keyboard users land where they started.
  • Rows are 28px, above the 24px minimum target.
Keyboard interactions
KeysAction
EnterOpens the menu, or runs the highlighted item.
SpaceOpens the menu, or runs the highlighted item.
↓↑Moves the highlight, wrapping at the ends. Down on the trigger opens the menu.
HomeHighlights the first item.
EndHighlights the last item.
→Opens the highlighted submenu.
←Closes the submenu and returns to its trigger.
EscCloses the menu and returns focus to the trigger.
A–ZJumps to the next item starting with that letter.

Design tokens#

Design tokens
TokenUsed for
--popoverSurface fill
--accentHighlighted item, open submenu trigger
--destructiveDestructive text, icon and its 10% and 20% tints
--muted-foregroundGroup labels and shortcuts
--borderSeparators
shadow-mdThe overlay shadow; submenus use shadow-lg
--radius-lg10px surface corners
--radius-md8px item corners

API reference#

DropdownMenu

The root. Holds the open state.

Other props spread onto Base UI Menu.Root.

Props of DropdownMenu
PropTypeDefaultDescription
openbooleanNo defaultControls whether it is open.
defaultOpenbooleanfalseWhether it starts open when uncontrolled.
onOpenChange(open: boolean, eventDetails) => voidNo defaultCalled when it opens or closes.
modalbooleantrueLocks page scroll and outside interaction while open.
loopFocusbooleantrueWraps the highlight at the ends.
highlightItemOnHoverbooleantrueMoves the highlight with the pointer.
disabledbooleanfalseStops the menu from opening.

DropdownMenuTrigger

The button that opens the menu.

Other props spread onto Base UI Menu.Trigger (<button>).

Props of DropdownMenuTrigger
PropTypeDefaultDescription
renderReactElement | (props, state) => ReactElementNo defaultRender your own button, such as <Button variant="ghost" size="icon-sm" />.
openOnHoverbooleanfalseAlso opens on hover, for menubars.
delaynumber100Hover delay with openOnHover.
disabledbooleanfalseDisables the trigger.

DropdownMenuContent

The surface, with its portal and positioner.

Other props spread onto Base UI Menu.Popup.

Props of DropdownMenuContent
PropTypeDefaultDescription
align"start" | "center" | "end""start"Alignment along the trigger. Row menus use end.
side"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"Which side of the trigger it opens on.
sideOffsetnumber4Distance from the trigger in pixels.
alignOffsetnumber0Offset along the alignment axis.
classNamestringNo defaultMerged after the defaults. Set a width such as w-56; otherwise the menu matches the trigger's width, at least 8rem.

DropdownMenuItem

An action.

Other props spread onto Base UI Menu.Item (<div>).

Props of DropdownMenuItem
PropTypeDefaultDescription
onClick(event) => voidNo defaultRuns the action. The menu then closes.
variant"default" | "destructive""default"destructive draws the item in Signal Red.
insetbooleanfalseIndents the label 28px to line up with items that have icons or checks.
disabledbooleanfalseDims the item and stops it running. It stays reachable by arrow keys.
closeOnClickbooleantrueCloses the menu after the click.
renderReactElement | (props, state) => ReactElementNo defaultRender a Next Link for items that navigate.

DropdownMenuCheckboxItem

An item that toggles on and off, with a check at the right.

Other props spread onto Base UI Menu.CheckboxItem.

Props of DropdownMenuCheckboxItem
PropTypeDefaultDescription
checkedbooleanNo defaultWhether it is checked, when controlled.
defaultCheckedbooleanfalseInitial state when uncontrolled.
onCheckedChange(checked: boolean, eventDetails) => voidNo defaultCalled when it toggles.
closeOnClickbooleanfalseKeeps the menu open so several can be toggled.
insetbooleanfalseIndents the label 28px.

DropdownMenuRadioGroup

Groups radio items that share one value.

Other props spread onto Base UI Menu.RadioGroup.

Props of DropdownMenuRadioGroup
PropTypeDefaultDescription
valueanyNo defaultThe selected value, when controlled.
defaultValueanyNo defaultInitial value when uncontrolled.
onValueChange(value: any, eventDetails) => voidNo defaultCalled when a radio item is chosen.

DropdownMenuRadioItem

One option in a radio group, with a check at the right when selected.

Other props spread onto Base UI Menu.RadioItem.

Props of DropdownMenuRadioItem
PropTypeDefaultDescription
valueRequiredanyNo defaultThe value it selects.
closeOnClickbooleanfalseCloses the menu after choosing.
insetbooleanfalseIndents the label 28px.

DropdownMenuSub

The root of a submenu.

Other props spread onto Base UI Menu.SubmenuRoot.

No props of its own.

DropdownMenuSubTrigger

The item that opens a submenu, with a trailing chevron.

Other props spread onto Base UI Menu.SubmenuTrigger.

Props of DropdownMenuSubTrigger
PropTypeDefaultDescription
insetbooleanfalseIndents the label 28px.
disabledbooleanfalseDisables the submenu.
delaynumber100Hover delay before it opens.

DropdownMenuSubContent

The submenu surface. Opens to the right, aligned to the trigger's top.

Other props spread onto DropdownMenuContent.

Props of DropdownMenuSubContent
PropTypeDefaultDescription
sidestring"right"Which side it opens on.
alignOffsetnumber-3Lines the first item up with the trigger.
classNamestringNo defaultSet a width such as w-48 or w-64.

DropdownMenuLabel

A 12px Slate Meta label for a group.

Other props spread onto Base UI Menu.GroupLabel.

Props of DropdownMenuLabel
PropTypeDefaultDescription
insetbooleanfalseIndents it 28px.

DropdownMenuGroup

Groups items under a label.

Other props spread onto Base UI Menu.Group.

No props of its own.

DropdownMenuSeparator

A Hairline between groups.

Other props spread onto Base UI Menu.Separator.

No props of its own.

DropdownMenuShortcut

A shortcut at the end of an item.

Other props spread onto <span>.

No props of its own.

DropdownMenuPortal

The portal, exported for custom compositions. DropdownMenuContent already includes it.

Other props spread onto Base UI Menu.Portal.

No props of its own.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

DropdownMenuContent defaults to the trigger's width (w-(--anchor-width), at least 8rem), so a menu opened from an icon button is 8rem wide and truncates. Nearly every call site passes a width such as w-56.

DropdownMenuShortcut sets tracking-widest, letter-spacing the shortcut against the no letter-spaced captions rule, and writes plain text where tooltips use Kbd chips. Only the app sidebar's help menu uses it.

Base UI's Menu.LinkItem isn't wrapped. Navigating items render a Link through DropdownMenuItem's render, which works but keeps the menuitem role on a link.

Submenus use shadow-lg, the dialog shadow, where DESIGN.md gives every menu the overlay shadow.

Menus enter from 0.97 but exit with zoom-out-95 to 0.95.