Use Dropdown Menu for commands associated with a button. Use Select for a form value. A menu item runs an application action; opening the menu does not fetch or save data.
import { BookOpen, Copy, FileJson, Palette, Plus, Settings, UploadCloud } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="noShadow" />}>
Registry actions
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64">
<DropdownMenuLabel>neobrutal-ui</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BookOpen aria-hidden="true" />
Open documentation
<DropdownMenuShortcut>Ctrl+D</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Copy aria-hidden="true" />
Copy install command
<DropdownMenuShortcut>Ctrl+C</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<FileJson aria-hidden="true" />
Inspect registry JSON
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette aria-hidden="true" />
Apply theme
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>Yellow</DropdownMenuItem>
<DropdownMenuItem>Blue</DropdownMenuItem>
<DropdownMenuItem>Rose</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>
<Plus aria-hidden="true" />
Add to collection
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Settings aria-hidden="true" />
Registry settings
</DropdownMenuItem>
<DropdownMenuItem disabled>
<UploadCloud aria-hidden="true" />
Publish release
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
#Installation
Complete the base setup. The CLI resolves dependencies. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/dropdown-menu.jsonnpm install @base-ui/react@^1.8.0Create components/ui/dropdown-menu.tsx:
"use client";
import { DirectionProvider } from "@base-ui/react/direction-provider";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
type DropdownMenuRootProps = MenuPrimitive.Root.Props & {
dir?: "ltr" | "rtl";
};
type DropdownMenuContentProps = MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
| "align"
| "alignOffset"
| "arrowPadding"
| "collisionAvoidance"
| "collisionBoundary"
| "collisionPadding"
| "disableAnchorTracking"
| "positionMethod"
| "side"
| "sideOffset"
| "sticky"
>;
function DropdownMenu({ dir, ...props }: DropdownMenuRootProps) {
const menu = <MenuPrimitive.Root {...props} />;
return dir ? <DirectionProvider direction={dir}>{menu}</DirectionProvider> : menu;
}
function DropdownMenuPortal(props: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger(props: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
align = "center",
alignOffset = 0,
arrowPadding = 0,
children,
className,
collisionAvoidance,
collisionBoundary,
collisionPadding = 0,
disableAnchorTracking,
positionMethod,
side = "bottom",
sideOffset = 4,
sticky,
...props
}: DropdownMenuContentProps) {
return (
<DropdownMenuPortal>
<MenuPrimitive.Positioner
align={align}
alignOffset={alignOffset}
arrowPadding={arrowPadding}
collisionAvoidance={collisionAvoidance}
collisionBoundary={collisionBoundary}
collisionPadding={collisionPadding}
disableAnchorTracking={disableAnchorTracking}
positionMethod={positionMethod}
side={side}
sideOffset={sideOffset}
sticky={sticky}
className="isolate z-50 outline-none"
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
{...props}
className={(state) =>
cn(
"z-50 min-w-[8rem] max-h-(--available-height) origin-(--transform-origin) overflow-y-auto rounded-base border-2 border-border bg-main p-1 font-base text-main-foreground duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
</MenuPrimitive.Popup>
</MenuPrimitive.Positioner>
</DropdownMenuPortal>
);
}
function DropdownMenuGroup(props: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<"div"> & {
inset?: boolean;
}) {
return (
<div
data-slot="dropdown-menu-label"
data-inset={inset}
role="presentation"
className={cn("px-2 py-1.5 text-sm font-heading data-inset:pl-8", className)}
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
{...props}
className={(state) =>
cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-base border-2 border-transparent bg-main px-2 py-1.5 text-sm font-base outline-none transition-colors select-none data-highlighted:outline-2 data-highlighted:outline-solid data-highlighted:-outline-offset-4 data-highlighted:outline-current data-inset:pl-8 data-[variant=destructive]:bg-destructive data-[variant=destructive]:text-destructive-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function DropdownMenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
children,
inset,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
{...props}
className={(state) =>
cn(
"flex cursor-default items-center gap-2 rounded-base border-2 border-transparent bg-main px-2 py-1.5 text-sm font-base outline-none select-none data-highlighted:outline-2 data-highlighted:outline-solid data-highlighted:-outline-offset-4 data-highlighted:outline-current data-inset:pl-8 data-popup-open:outline-2 data-popup-open:outline-solid data-popup-open:-outline-offset-4 data-popup-open:outline-current [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: DropdownMenuContentProps) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className={(state) =>
cn(
"w-auto min-w-[8rem] rounded-base border-2 border-border bg-main p-1 font-base text-main-foreground duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
typeof className === "function" ? className(state) : className,
)
}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
closeOnClick = true,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
closeOnClick={closeOnClick}
{...props}
className={(state) =>
cn(
"relative flex cursor-default items-center gap-2 rounded-base border-2 border-transparent py-1.5 pr-2 pl-8 text-sm font-base outline-none transition-colors select-none data-highlighted:outline-2 data-highlighted:outline-solid data-highlighted:-outline-offset-4 data-highlighted:outline-current data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
typeof className === "function" ? className(state) : className,
)
}
>
<span
className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
}
function DropdownMenuRadioItem({
className,
children,
closeOnClick = true,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
closeOnClick={closeOnClick}
{...props}
className={(state) =>
cn(
"relative flex cursor-default items-center gap-2 rounded-base border-2 border-transparent py-1.5 pr-2 pl-8 text-sm font-base outline-none transition-colors select-none data-highlighted:outline-2 data-highlighted:outline-solid data-highlighted:-outline-offset-4 data-highlighted:outline-current data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
typeof className === "function" ? className(state) : className,
)
}
>
<span
className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
}
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
{...props}
className={(state) =>
cn(
"-mx-1 my-1 h-0.5 bg-border",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn("ml-auto text-xs font-base tracking-widest", className)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
The trigger example also uses Button.
#Usage
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
export default function ProjectActions() {
const [message, setMessage] = useState("No action selected.");
return (
<div className="grid gap-3">
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
Project actions
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setMessage("Rename selected in this preview.")}>
Rename
</DropdownMenuItem>
<DropdownMenuItem disabled>Export</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<p role="status">{message}</p>
</div>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
DropdownMenu.open / onOpenChange | Controlled visibility | Uncontrolled |
DropdownMenuTrigger.render | Compose the trigger button | Primitive button |
DropdownMenuContent.side / align | Popup position | bottom / center |
DropdownMenuItem.onClick / disabled | Action and availability | — / false |
DropdownMenuCheckboxItem.checked / onCheckedChange | Independent checked state | Uncontrolled |
DropdownMenuRadioGroup.value / onValueChange | Mutually exclusive choice | Uncontrolled |
DropdownMenuSub | Nested menu | — |
DropdownMenuContent.alignOffset / arrowPadding / collisionPadding | Alignment, arrow, and collision-boundary spacing, in pixels | 0 |
DropdownMenuContent.sideOffset | Trigger-to-popup gap, in pixels | 4 |
DropdownMenuSubContent.side / align | Preferred submenu placement and alignment | right / start |
DropdownMenuSubContent.alignOffset / sideOffset | Submenu alignment adjustment and gap, in pixels | -3 / 0 |
DropdownMenuItem.variant | Item appearance: default or destructive | default |
DropdownMenuCheckboxItem.closeOnClick / DropdownMenuRadioItem.closeOnClick | Close after selecting a stateful item | true |
#Accessibility
Use an explicit trigger name and clear action labels. Arrow keys navigate items, Enter selects, and Escape dismisses. Displayed shortcut text does not implement a shortcut. For destructive actions, describe the consequence and use confirmation when warranted. Do not turn a menu into a complex form with unrelated focusable controls.
Menu content is bounded by Base UI's available height. Long menus scroll so pointer users and keyboard users can reach every item.
#Examples
#Checkboxes
"use client";
import { CreditCardIcon, LogOutIcon, Settings2Icon, UserIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function DropdownMenuCheckboxes() {
const [showStatusBar, setShowStatusBar] = React.useState(true);
const [showActivityBar, setShowActivityBar] = React.useState(false);
const [showPanel, setShowPanel] = React.useState(false);
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="noShadow" />}>Checkboxes</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuGroup>
<DropdownMenuLabel>Account</DropdownMenuLabel>
<DropdownMenuItem>
<UserIcon /> Profile
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon /> Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Settings2Icon /> Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
<DropdownMenuCheckboxItem checked={showStatusBar} onCheckedChange={setShowStatusBar}>
Status Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showActivityBar}
onCheckedChange={setShowActivityBar}
disabled
>
Activity Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={showPanel} onCheckedChange={setShowPanel}>
Panel
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<LogOutIcon /> Sign Out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
#Radio options
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function DropdownMenuRadioGroupDemo() {
const [position, setPosition] = React.useState("bottom");
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="noShadow" />}>Radio Group</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel inset>Panel Position</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="right" disabled>
Right
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}