Dropdown Menu
A dropdown menu is a list of actions that opens from a button. It keeps rarely used commands one click away without giving each one a button on the page.
import { ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="secondary" />}>
Account
<ChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem disabled>Billing</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem defaultChecked>Show status bar</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup defaultValue="system">
<DropdownMenuRadioItem value="light">Light</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">System</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { Check } from "lucide-react";
import { Menu as BaseMenu } from "@base-ui/react/menu";
import { cx } from "@/lib/cx";
import { menuItemStyles, menuItemTones, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
export const DropdownMenu = BaseMenu.Root;
// Renders a button by default. To open it from your own element, such as a Button, pass it as
// render: <DropdownMenuTrigger render={<Button />}>.
export const DropdownMenuTrigger = BaseMenu.Trigger;
// side, sideOffset and align place the menu against its trigger; every other prop goes to the
// popup itself.
export function DropdownMenuContent({
side = "bottom",
sideOffset = 6,
align = "start",
className,
...props
}: WithClass<ComponentProps<typeof BaseMenu.Popup>> &
Pick<ComponentProps<typeof BaseMenu.Positioner>, "side" | "sideOffset" | "align">) {
return (
<BaseMenu.Portal>
<BaseMenu.Positioner side={side} sideOffset={sideOffset} align={align} className="z-50">
<BaseMenu.Popup className={cx(popupSurface, popupMotion, "min-w-48 p-1", className)} {...props} />
</BaseMenu.Positioner>
</BaseMenu.Portal>
);
}
// variant="destructive" turns the row red, for something like Log out or Delete.
export function DropdownMenuItem({
variant = "default",
className,
...props
}: WithClass<ComponentProps<typeof BaseMenu.Item>> & { variant?: keyof typeof menuItemTones }) {
return <BaseMenu.Item className={cx(menuItemStyles, menuItemTones[variant], className)} {...props} />;
}
export function DropdownMenuSeparator({ className, ...props }: WithClass<ComponentProps<typeof BaseMenu.Separator>>) {
return <BaseMenu.Separator className={cx("-mx-1 my-1 h-px bg-border", className)} {...props} />;
}
// A group with a label. Put DropdownMenuLabel inside a DropdownMenuGroup.
export const DropdownMenuGroup = BaseMenu.Group;
export function DropdownMenuLabel({ className, ...props }: WithClass<ComponentProps<typeof BaseMenu.GroupLabel>>) {
return <BaseMenu.GroupLabel className={cx("px-2 py-1.5 text-sm font-medium text-text-secondary", className)} {...props} />;
}
// A row that can be ticked on and off. The tick shows in the space on the left.
export function DropdownMenuCheckboxItem({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseMenu.CheckboxItem>>) {
return (
<BaseMenu.CheckboxItem className={cx(menuItemStyles, menuItemTones.default, "relative pl-8", className)} {...props}>
<span className="absolute left-2 flex size-4 items-center justify-center">
<BaseMenu.CheckboxItemIndicator>
<Check aria-hidden className="size-4" />
</BaseMenu.CheckboxItemIndicator>
</span>
{children}
</BaseMenu.CheckboxItem>
);
}
// Radio rows, where exactly one is chosen. Wrap the DropdownMenuRadioItems in a
// DropdownMenuRadioGroup.
export const DropdownMenuRadioGroup = BaseMenu.RadioGroup;
export function DropdownMenuRadioItem({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseMenu.RadioItem>>) {
return (
<BaseMenu.RadioItem className={cx(menuItemStyles, menuItemTones.default, "relative pl-8", className)} {...props}>
<span className="absolute left-2 flex size-4 items-center justify-center">
<BaseMenu.RadioItemIndicator>
<span className="block size-2 rounded-full bg-text" />
</BaseMenu.RadioItemIndicator>
</span>
{children}
</BaseMenu.RadioItem>
);
}
// A keyboard shortcut at the right end of a row, such as <Kbd> or plain text.
export function DropdownMenuShortcut({ className, ...props }: ComponentProps<"span">) {
return <span className={cx("ml-auto pl-6 text-sm text-text-secondary", className)} {...props} />;
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "default" | "destructive" | "default" | On DropdownMenuItem. The destructive variant turns the row red, for something like Log out or Delete. |
onClick | function | — | On DropdownMenuItem. Runs when the item is chosen. The menu closes afterwards. |
disabled | boolean | false | On any item. Dims it and stops it from being chosen. The arrow keys can still reach it. |
defaultChecked | boolean | — | On DropdownMenuCheckboxItem. Whether it starts ticked. Use checked and onCheckedChange to control it. |
defaultValue | string | — | On DropdownMenuRadioGroup. The value of the radio item that starts chosen. Each DropdownMenuRadioItem needs a value. |
side | "top" | "right" | "bottom" | "left" | "bottom" | On DropdownMenuContent. Which side of the button it opens on. It flips to the other side if there is no room. |
align | "start" | "center" | "end" | "start" | On DropdownMenuContent. How it lines up along that side. |
render | element | — | On DropdownMenuTrigger. Opens the menu from your own element, such as a Button. |
Every other prop goes to the Base UI Menu underneath.
The thinking
A menu answers the question: what can I do here? It lists actions, so choosing a row does something and the menu goes away.
- Built on Base UI: Base UI handles the open state, the placement, the arrow-key navigation and type-ahead, and it closes the menu when an item is chosen.
- One row style: The Dropdown Menu and the Context Menu share one look for their rows, so a command looks the same wherever you meet it.
- Row types: A plain item runs an action. A checkbox item switches something on or off. Radio items choose one of several. Groups and separators keep a long menu readable.
- Menu or Select: A menu runs commands. A Select picks a value for a form field.
Interface design
- Surface: The shared overlay surface with 4px of padding, so a highlighted row sits inside the border instead of touching it.
- Rows: Each row has the sm radius and a light wash when highlighted, whether by the pointer or the keyboard.
- Marks on the left: Ticks and radio dots sit in a column on the left. Shortcuts sit at the right end of the row.
- Destructive: Red text and a red wash, so a dangerous row can be told apart before reading it.
- Disabled: The row text uses the muted color and the not-allowed cursor.
Interaction
- Open: Click the button, or press Enter, Space or the Down arrow when it has focus.
- Keyboard: The arrow keys move through the rows, Home and End jump to the first and last, and typing a letter jumps to a row that starts with it. Enter or Space chooses a row.
- Close: Escape, a click outside, or choosing an item. Checkbox and radio items keep it open until you leave.
- Focus: When the menu closes, focus goes back to the button.
Empathy
- Announced as a menu: The button reports that it opens a menu, and each row is announced with its role. Checkbox and radio rows report whether they are on.
- Not only a pointer thing: Everything works with the keyboard alone.
- Don't hide the main action: Put the thing most people came to do on the page. A menu is for the rest.
- Words, not only icons: Rows are labeled with text so they can be found by reading, searching and typing.