DropdownMenu provides Radix-powered menu primitives with Brandit UI styling
for action menus, view controls, and contextual settings.
When to use
- Show compact action lists anchored to a trigger.
- Provide toggleable preferences (
CheckboxItem/RadioItem). - Group related actions with labels, separators, and nested submenus.
Behavior notes
- Use
DropdownMenuTriggerwithasChildfor custom trigger components. DropdownMenuItemsupportsvariant='destructive'for dangerous actions.DropdownMenuCheckboxItemsupports checkbox and radio indicator styles.DropdownMenuSub+DropdownMenuSubTrigger+DropdownMenuSubContentprovide nested option flows.
Example
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm'>
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Row actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Open profile</DropdownMenuItem>
<DropdownMenuItem>Copy ID</DropdownMenuItem>
<DropdownMenuItem variant='destructive'>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Accessibility notes
- Keyboard navigation and focus management are provided by Radix primitives.
- Use concise labels and separators to keep command groups scannable.
Preview
Usage
import { ChevronDownIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from '@branditdev/ui';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from './dropdown-menu';
export function DropdownMenuDemo() {
const [showStatusBar, setShowStatusBar] = useState(true);
const [showLineNumbers, setShowLineNumbers] = useState(false);
const [density, setDensity] = useState('comfortable');
return (
<div className='flex flex-wrap items-center gap-3'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm'>
Display options
<ChevronDownIcon className='size-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-64'>
<DropdownMenuLabel>Workspace</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem>
New file
<DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Duplicate
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={showStatusBar}
onCheckedChange={checked => setShowStatusBar(Boolean(checked))}
variant='checkbox'
>
Show status bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showLineNumbers}
onCheckedChange={checked => setShowLineNumbers(Boolean(checked))}
variant='checkbox'
>
Show line numbers
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Density</DropdownMenuLabel>
<DropdownMenuRadioGroup value={density} onValueChange={setDensity}>
<DropdownMenuRadioItem value='compact'>
Compact
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value='comfortable'>
Comfortable
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value='spacious'>
Spacious
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Export</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Export CSV</DropdownMenuItem>
<DropdownMenuItem>Export JSON</DropdownMenuItem>
<DropdownMenuItem>Export PDF</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem variant='destructive'>
Delete workspace
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
API
DropdownMenu(
props):Element
Defined in: components/dropdown-menu/dropdown-menu.tsx:13
Dropdown menu root for trigger/content composition.
Parameters
| Name | Type | Optional | Default | Description |
|---|---|---|---|---|
| props | DropdownMenuProps | no | - | Dropdown root props. |
Returns
Element
A dropdown menu root wrapper.
Source
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
import * as React from 'react';
import { cn } from '@/lib/utils';
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot='dropdown-menu-trigger'
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot='dropdown-menu-content'
sideOffset={sideOffset}
className={cn(
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot='dropdown-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent dark:focus:bg-accent/20 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
className
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
variant,
orientation = 'left',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
variant?: 'checkbox' | 'radio';
orientation?: 'left' | 'right';
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot='dropdown-menu-checkbox-item'
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 text-sm outline-hidden select-none focus:bg-accent dark:focus:bg-accent/20 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variant === 'checkbox' ? 'pr-2 pl-8' : 'pr-8 pl-2',
className
)}
checked={checked}
{...props}
>
<span
className={cn(
'pointer-events-none absolute flex size-3.5 items-center justify-center',
orientation === 'left' ? 'left-2' : 'right-2'
)}
>
<DropdownMenuPrimitive.ItemIndicator>
{variant === 'checkbox' ? (
<CheckIcon className='size-4' />
) : (
<CircleIcon className='size-2 fill-primary stroke-primary' />
)}
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot='dropdown-menu-radio-group'
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot='dropdown-menu-radio-item'
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent dark:focus:bg-accent/20 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className='size-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot='dropdown-menu-label'
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-inset:pl-8',
className
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot='dropdown-menu-separator'
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='dropdown-menu-shortcut'
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot='dropdown-menu-sub-trigger'
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent dark:focus:bg-accent/20 focus:text-accent-foreground data-inset:pl-8 data-[state=open]:bg-accent dark:data-[state=open]:bg-accent/20 data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
>
{children}
<ChevronRightIcon className='ml-auto size-4' />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot='dropdown-menu-sub-content'
className={cn(
'z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};