Skip to main content

Dropdown Menu

Group secondary actions. Save screen space. Keep UI clean.

1'use client';
2
3import {
4 CreditCardIcon,
5 Key01Icon,
6 Logout02Icon,
7 Notification03Icon,
8 Settings01Icon,
9 UserGroupIcon,
10 UserIcon,
11} from '@hugeicons/core-free-icons';
12import { HugeiconsIcon } from '@hugeicons/react';
13import { Avatar } from '@/components/ui/avatar';
14import { Button } from '@/components/ui/button';
15import { DropdownMenu } from '@/components/ui/dropdown-menu';
16
17const menuGroups = [
18 {
19 label: 'Account',
20 items: [
21 { icon: UserIcon, label: 'Profile', shortcut: '⇧⌘P' },
22 { icon: Notification03Icon, label: 'Notifications', shortcut: null },
23 { icon: Settings01Icon, label: 'Settings', shortcut: '⌘,' },
24 ],
25 },
26 {
27 label: 'Northwind',
28 items: [
29 { icon: UserGroupIcon, label: 'Team members', shortcut: null },
30 { icon: CreditCardIcon, label: 'Usage and billing', shortcut: null },
31 { icon: Key01Icon, label: 'API keys', shortcut: null },
32 ],
33 },
34];
35
36export function Default() {
37 return (
38 <DropdownMenu>
39 <DropdownMenu.Trigger asChild>
40 <Button variant="outline">Daniela Rojas</Button>
41 </DropdownMenu.Trigger>
42 <DropdownMenu.Content className="w-64 max-w-[calc(100vw-2rem)]" align="start">
43 <div className="flex items-center gap-3 px-3 py-2">
44 <Avatar size="sm">
45 <Avatar.Fallback>DR</Avatar.Fallback>
46 </Avatar>
47 <div className="min-w-0">
48 <p className="truncate text-sm font-medium">Daniela Rojas</p>
49 <p className="text-muted-foreground truncate text-xs">daniela@northwind.io</p>
50 </div>
51 </div>
52 {menuGroups.map((group) => (
53 <div key={group.label}>
54 <DropdownMenu.Separator />
55 <DropdownMenu.Label>{group.label}</DropdownMenu.Label>
56 {group.items.map((item) => (
57 <DropdownMenu.Item key={item.label} className="justify-between">
58 <span className="flex items-center">
59 <HugeiconsIcon icon={item.icon} className="mr-2 size-4" />
60 {item.label}
61 </span>
62 {item.shortcut && (
63 <span className="text-muted-foreground text-xs">{item.shortcut}</span>
64 )}
65 </DropdownMenu.Item>
66 ))}
67 </div>
68 ))}
69 <DropdownMenu.Separator />
70 <DropdownMenu.Item variant="destructive">
71 <HugeiconsIcon icon={Logout02Icon} className="mr-2 size-4" />
72 Sign out
73 </DropdownMenu.Item>
74 </DropdownMenu.Content>
75 </DropdownMenu>
76 );
77}

Installation

pnpm dlx nachui add dropdown-menu

Anatomy

1import { DropdownMenu } from '@/components/ui/dropdown-menu';
1<DropdownMenu>
2 <DropdownMenu.Trigger asChild>
3 <Button variant="outline">Open Menu</Button>
4 </DropdownMenu.Trigger>
5 <DropdownMenu.Content>
6 <DropdownMenu.Label>My Account</DropdownMenu.Label>
7 <DropdownMenu.Item>Profile</DropdownMenu.Item>
8 <DropdownMenu.Item>Settings</DropdownMenu.Item>
9 <DropdownMenu.Separator />
10 <DropdownMenu.Item variant="destructive">Logout</DropdownMenu.Item>
11 </DropdownMenu.Content>
12</DropdownMenu>

Composition

Use the following composition to build a DropdownMenu:
DropdownMenu
├── DropdownMenu.Trigger
└── DropdownMenu.Content
├── DropdownMenu.Label
├── DropdownMenu.Item
└── DropdownMenu.Separator

Variants

Checkboxes

1'use client';
2
3import { Tick02Icon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import * as React from 'react';
6import { Button } from '@/components/ui/button';
7import { DropdownMenu } from '@/components/ui/dropdown-menu';
8
9const COLUMNS = [
10 { id: 'customer', label: 'Customer' },
11 { id: 'status', label: 'Status' },
12 { id: 'amount', label: 'Amount' },
13 { id: 'method', label: 'Payment method' },
14 { id: 'created', label: 'Created' },
15] as const;
16
17const INITIAL_STATE: Record<string, boolean> = {
18 customer: true,
19 status: true,
20 amount: true,
21 method: false,
22 created: false,
23};
24
25export function Checkboxes() {
26 const [visible, setVisible] = React.useState<Record<string, boolean>>(INITIAL_STATE);
27
28 const toggle = (id: string) => setVisible((prev) => ({ ...prev, [id]: !prev[id] }));
29 const shownCount = COLUMNS.filter((column) => visible[column.id]).length;
30
31 return (
32 <DropdownMenu>
33 <DropdownMenu.Trigger asChild>
34 <Button variant="outline">
35 Columns ({shownCount}/{COLUMNS.length})
36 </Button>
37 </DropdownMenu.Trigger>
38 <DropdownMenu.Content className="w-56 max-w-[calc(100vw-2rem)]" align="start">
39 <DropdownMenu.Label>Show in orders table</DropdownMenu.Label>
40 <DropdownMenu.Separator />
41 {COLUMNS.map(({ id, label }) => (
42 <DropdownMenu.Item key={id} onClick={() => toggle(id)}>
43 <span className="flex w-6 items-center justify-center">
44 {visible[id] && <HugeiconsIcon icon={Tick02Icon} size={16} />}
45 </span>
46 {label}
47 </DropdownMenu.Item>
48 ))}
49 </DropdownMenu.Content>
50 </DropdownMenu>
51 );
52}

Radio Group

1'use client';
2
3import * as React from 'react';
4import { Button } from '@/components/ui/button';
5import { DropdownMenu } from '@/components/ui/dropdown-menu';
6
7const SORT_OPTIONS = [
8 { value: 'newest', label: 'Newest first' },
9 { value: 'oldest', label: 'Oldest first' },
10 { value: 'amount-desc', label: 'Amount, high to low' },
11 { value: 'amount-asc', label: 'Amount, low to high' },
12] as const;
13
14export function RadioGroup() {
15 const [sort, setSort] = React.useState<string>('newest');
16 const active = SORT_OPTIONS.find((option) => option.value === sort);
17
18 return (
19 <DropdownMenu>
20 <DropdownMenu.Trigger asChild>
21 <Button variant="outline">Sort: {active?.label}</Button>
22 </DropdownMenu.Trigger>
23 <DropdownMenu.Content className="w-56 max-w-[calc(100vw-2rem)]" align="start">
24 <DropdownMenu.Label>Sort 240 orders by</DropdownMenu.Label>
25 <DropdownMenu.Separator />
26 {SORT_OPTIONS.map((option) => (
27 <DropdownMenu.Item key={option.value} onClick={() => setSort(option.value)}>
28 <span className="flex w-6 items-center justify-center">
29 {sort === option.value && <div className="bg-foreground size-2 rounded-full" />}
30 </span>
31 {option.label}
32 </DropdownMenu.Item>
33 ))}
34 </DropdownMenu.Content>
35 </DropdownMenu>
36 );
37}

API Reference

PropTypeDefaultDescription
defaultOpenbooleanfalseInitial open state
onOpenChange(open: boolean) => void-Callback when open state changes
classNamestring-Additional CSS classes
PropTypeDefaultDescription
asChildbooleanfalseRender as child element
classNamestring-Additional CSS classes
PropTypeDefaultDescription
align'start' | 'center' | 'end''start'Horizontal alignment
sideOffsetnumber6Distance from trigger
classNamestring-Additional CSS classes
PropTypeDefaultDescription
disabledbooleanfalseDisable the item
variant'default' | 'destructive''default'Visual style
onSelect() => void-Callback when selected
classNamestring-Additional CSS classes
PropTypeDefaultDescription
childrenReactNode-Label text
Visual separator between menu items.
No props - renders a horizontal line.
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I