Saltar al contenido principal

Dropdown Menu

Agrupar acciones secundarias. Ahorrar espacio. Mantener UI limpia.

default.tsx
1'use client';
2
3import {
4 KeyboardIcon,
5 Logout02Icon,
6 Notification03Icon,
7 Settings01Icon,
8 Shield02Icon,
9 UserIcon,
10} from '@hugeicons/core-free-icons';
11import { HugeiconsIcon } from '@hugeicons/react';
12import { Button } from '@/components/ui/button';
13import { DropdownMenu } from '@/components/ui/dropdown-menu';
14
15const menuGroups = [
16 {
17 label: 'Account',
18 items: [
19 { icon: UserIcon, label: 'Profile' },
20 { icon: Notification03Icon, label: 'Notifications' },
21 { icon: Settings01Icon, label: 'Settings' },
22 ],
23 },
24 {
25 label: 'Security',
26 items: [
27 { icon: Shield02Icon, label: 'Privacy & Security' },
28 { icon: KeyboardIcon, label: 'Keyboard shortcuts' },
29 ],
30 },
31];
32
33const actionItems = [{ icon: Logout02Icon, label: 'Sign out', destructive: true }];
34
35export function Default() {
36 return (
37 <DropdownMenu>
38 <DropdownMenu.Trigger asChild>
39 <Button variant="outline">Account</Button>
40 </DropdownMenu.Trigger>
41 <DropdownMenu.Content className="w-56" align="start">
42 {menuGroups.map((group, groupIdx) => (
43 <div key={groupIdx}>
44 <DropdownMenu.Label>{group.label}</DropdownMenu.Label>
45 {group.items.map((item, itemIdx) => (
46 <DropdownMenu.Item key={itemIdx}>
47 <HugeiconsIcon icon={item.icon} className="mr-2 size-4" />
48 {item.label}
49 </DropdownMenu.Item>
50 ))}
51 <DropdownMenu.Separator />
52 </div>
53 ))}
54 {actionItems.map((item, idx) => (
55 <DropdownMenu.Item
56 key={idx}
57 className={item.destructive ? 'text-destructive focus:text-destructive' : ''}
58 >
59 <HugeiconsIcon icon={item.icon} className="mr-2 size-4" />
60 {item.label}
61 </DropdownMenu.Item>
62 ))}
63 </DropdownMenu.Content>
64 </DropdownMenu>
65 );
66}

Instalación

pnpm dlx nachui add dropdown-menu

Anatomía

1import { DropdownMenu } from '@/components/ui/dropdown-menu';
1<DropdownMenu>
2 <DropdownMenu.Trigger asChild>
3 <Button variant="outline">Abrir Menú</Button>
4 </DropdownMenu.Trigger>
5 <DropdownMenu.Content>
6 <DropdownMenu.Label>Mi Cuenta</DropdownMenu.Label>
7 <DropdownMenu.Item>Perfil</DropdownMenu.Item>
8 <DropdownMenu.Item>Configuración</DropdownMenu.Item>
9 <DropdownMenu.Separator />
10 <DropdownMenu.Item variant="destructive">Cerrar Sesión</DropdownMenu.Item>
11 </DropdownMenu.Content>
12</DropdownMenu>

Variantes

Casillas de verificación (Checkboxes)

checkboxes.tsx
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 ITEMS = [
10 { id: 'status-bar', label: 'Status Bar' },
11 { id: 'activity-bar', label: 'Activity Bar' },
12 { id: 'panel', label: 'Panel' },
13] as const;
14
15const INITIAL_STATE: Record<string, boolean> = {
16 'status-bar': true,
17 'activity-bar': false,
18 panel: false,
19};
20
21export function Checkboxes() {
22 const [checked, setChecked] = React.useState<Record<string, boolean>>(INITIAL_STATE);
23
24 const toggle = (id: string) => setChecked((prev) => ({ ...prev, [id]: !prev[id] }));
25
26 return (
27 <DropdownMenu>
28 <DropdownMenu.Trigger asChild>
29 <Button variant="outline">View Options</Button>
30 </DropdownMenu.Trigger>
31 <DropdownMenu.Content className="w-56" align="start">
32 <DropdownMenu.Label>Appearance</DropdownMenu.Label>
33 <DropdownMenu.Separator />
34 {ITEMS.map(({ id, label }) => (
35 <DropdownMenu.Item key={id} onClick={() => toggle(id)}>
36 <span className="flex w-6 items-center justify-center">
37 {checked[id] && <HugeiconsIcon icon={Tick02Icon} size={16} />}
38 </span>
39 {label}
40 </DropdownMenu.Item>
41 ))}
42 </DropdownMenu.Content>
43 </DropdownMenu>
44 );
45}

Grupo de radio (Radio Group)

radio-group.tsx
1'use client';
2
3import * as React from 'react';
4import { Button } from '@/components/ui/button';
5import { DropdownMenu } from '@/components/ui/dropdown-menu';
6
7export function RadioGroup() {
8 const [position, setPosition] = React.useState('bottom');
9
10 return (
11 <DropdownMenu>
12 <DropdownMenu.Trigger asChild>
13 <Button variant="outline">Panel Position</Button>
14 </DropdownMenu.Trigger>
15 <DropdownMenu.Content className="w-56" align="start">
16 <DropdownMenu.Label>Panel Position</DropdownMenu.Label>
17 <DropdownMenu.Separator />
18 {['Top', 'Bottom', 'Right', 'Left'].map((pos) => {
19 const value = pos.toLowerCase();
20 const isSelected = position === value;
21 return (
22 <DropdownMenu.Item key={value} onClick={() => setPosition(value)}>
23 <span className="flex w-6 items-center justify-center">
24 {isSelected && <div className="bg-foreground size-2 rounded-full" />}
25 </span>
26 {pos}
27 </DropdownMenu.Item>
28 );
29 })}
30 </DropdownMenu.Content>
31 </DropdownMenu>
32 );
33}

Referencia de API

PropTipoPor defectoDescripción
defaultOpenbooleanfalseEstado abierto inicial
onOpenChange(open: boolean) => void-Callback cuando cambia el estado
classNamestring-Clases CSS adicionales
PropTipoPor defectoDescripción
asChildbooleanfalseRenderizar como elemento hijo
classNamestring-Clases CSS adicionales
PropTipoPor defectoDescripción
align'start' | 'center' | 'end''start'Alineación horizontal
sideOffsetnumber6Distancia del activador
classNamestring-Clases CSS adicionales
PropTipoPor defectoDescripción
disabledbooleanfalseDeshabilitar el elemento
variant'default' | 'destructive''default'Estilo visual
onSelect() => void-Callback cuando se selecciona
classNamestring-Clases CSS adicionales
PropTipoPor defectoDescripción
childrenReactNode-Texto de la etiqueta
Separador visual entre elementos del menú.
Sin props - renderiza una línea horizontal.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue