DropdownMenu
Hoja clienteA menu of actions triggered by a button.
Vista previa en vivo
Renderizado desde el código fuente instalado de VEXA — los mismos archivos que recibes.
Instalación
Añade el código del componente a tu proyecto. Las dependencias del registry se resuelven automáticamente.
npx shadcn@latest add https://vexa.valfiguer.com/r/dropdown-menu.jsonSe instala en components/vexa/dropdown-menu.tsx
Dependencias npm
- @radix-ui/react-dropdown-menu
Dependencias del registry
- @vexa/utils
Otros items de VEXA que necesita este componente. Instálalos primero — la CLI los resuelve automáticamente.
Código
Cópialo a su ruta destino, o instálalo con el comando de arriba.
"use client";
import type * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "@/lib/utils";
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
className={className}
>
<path
d="M4 12.5 9 17.5 20 6.5"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function DotIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className={className}
>
<circle cx="12" cy="12" r="4" />
</svg>
);
}
function ChevronRightIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
className={className}
>
<path
d="m9 6 6 6-6 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/**
* DropdownMenu — a menu of actions triggered by a button.
*
* Client leaf. Radix implements the APG menu-button pattern: the trigger has
* `aria-haspopup`/`aria-expanded`, arrow keys move through items with type-ahead,
* Escape closes and returns focus to the trigger, and submenus open on
* ArrowRight. Use it for commands and actions — not for selecting a form value
* (use `Select`) or for navigation-only lists.
*/
export function DropdownMenu(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>,
) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
export function DropdownMenuTrigger(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>,
) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
export function DropdownMenuGroup(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>,
) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
export function DropdownMenuPortal(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>,
) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
export function DropdownMenuRadioGroup(
props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>,
) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
const menuContentClasses = cn(
"z-50 min-w-[8rem] overflow-hidden rounded-vexa-md border border-vexa-border bg-vexa-surface-raised p-1 text-vexa-foreground shadow-vexa-lg",
"transition-[opacity,transform] duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)] motion-reduce:transition-none",
"data-[state=closed]:scale-95 data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=open]:opacity-100",
);
export function DropdownMenuContent({
className,
sideOffset = 6,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(menuContentClasses, className)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
const menuItemClasses = cn(
"relative flex cursor-default select-none items-center gap-2 rounded-vexa-sm px-2 py-1.5 text-sm outline-none",
"transition-colors duration-[var(--vexa-duration-fast)]",
"focus:bg-vexa-accent focus:text-vexa-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&_svg]:size-4 [&_svg]:shrink-0",
);
export function DropdownMenuItem({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset ? "" : undefined}
className={cn(menuItemClasses, inset && "pl-8", className)}
{...props}
/>
);
}
export function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
checked={checked}
className={cn(menuItemClasses, "pl-8", className)}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
export function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(menuItemClasses, "pl-8", className)}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotIcon className="size-2.5" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
export function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset ? "" : undefined}
className={cn(
"px-2 py-1.5 text-xs font-medium text-vexa-muted-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
);
}
export 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-vexa-border", className)}
{...props}
/>
);
}
export function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-vexa-muted-foreground",
className,
)}
{...props}
/>
);
}
export function DropdownMenuSub(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>,
) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
export 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 ? "" : undefined}
className={cn(
menuItemClasses,
"data-[state=open]:bg-vexa-accent data-[state=open]:text-vexa-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
export function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(menuContentClasses, className)}
{...props}
/>
);
}
JSON del registryr/dropdown-menu.json
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "dropdown-menu",
"type": "registry:ui",
"title": "DropdownMenu",
"description": "A menu of actions triggered by a button.",
"dependencies": [
"@radix-ui/react-dropdown-menu"
],
"registryDependencies": [
"@vexa/utils"
],
"files": [
{
"path": "registry/default/ui/dropdown-menu.tsx",
"type": "registry:ui",
"target": "components/vexa/dropdown-menu.tsx",
"content": "\"use client\";\n\nimport type * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction CheckIcon({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={className}\n >\n <path\n d=\"M4 12.5 9 17.5 20 6.5\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n\nfunction DotIcon({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"4\" />\n </svg>\n );\n}\n\nfunction ChevronRightIcon({ className }: { className?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={className}\n >\n <path\n d=\"m9 6 6 6-6 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n\n/**\n * DropdownMenu — a menu of actions triggered by a button.\n *\n * Client leaf. Radix implements the APG menu-button pattern: the trigger has\n * `aria-haspopup`/`aria-expanded`, arrow keys move through items with type-ahead,\n * Escape closes and returns focus to the trigger, and submenus open on\n * ArrowRight. Use it for commands and actions — not for selecting a form value\n * (use `Select`) or for navigation-only lists.\n */\nexport function DropdownMenu(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>,\n) {\n return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nexport function DropdownMenuTrigger(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>,\n) {\n return (\n <DropdownMenuPrimitive.Trigger\n data-slot=\"dropdown-menu-trigger\"\n {...props}\n />\n );\n}\n\nexport function DropdownMenuGroup(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>,\n) {\n return (\n <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n );\n}\n\nexport function DropdownMenuPortal(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>,\n) {\n return (\n <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n );\n}\n\nexport function DropdownMenuRadioGroup(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>,\n) {\n return (\n <DropdownMenuPrimitive.RadioGroup\n data-slot=\"dropdown-menu-radio-group\"\n {...props}\n />\n );\n}\n\nconst menuContentClasses = cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-vexa-md border border-vexa-border bg-vexa-surface-raised p-1 text-vexa-foreground shadow-vexa-lg\",\n \"transition-[opacity,transform] duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)] motion-reduce:transition-none\",\n \"data-[state=closed]:scale-95 data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=open]:opacity-100\",\n);\n\nexport function DropdownMenuContent({\n className,\n sideOffset = 6,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(menuContentClasses, className)}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n\nconst menuItemClasses = cn(\n \"relative flex cursor-default select-none items-center gap-2 rounded-vexa-sm px-2 py-1.5 text-sm outline-none\",\n \"transition-colors duration-[var(--vexa-duration-fast)]\",\n \"focus:bg-vexa-accent focus:text-vexa-accent-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n \"[&_svg]:size-4 [&_svg]:shrink-0\",\n);\n\nexport function DropdownMenuItem({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset ? \"\" : undefined}\n className={cn(menuItemClasses, inset && \"pl-8\", className)}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n checked={checked}\n className={cn(menuItemClasses, \"pl-8\", className)}\n {...props}\n >\n <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(menuItemClasses, \"pl-8\", className)}\n {...props}\n >\n <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <DotIcon className=\"size-2.5\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nexport function DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n data-inset={inset ? \"\" : undefined}\n className={cn(\n \"px-2 py-1.5 text-xs font-medium text-vexa-muted-foreground\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-vexa-border\", className)}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"ml-auto text-xs tracking-widest text-vexa-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuSub(\n props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>,\n) {\n return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nexport function DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset ? \"\" : undefined}\n className={cn(\n menuItemClasses,\n \"data-[state=open]:bg-vexa-accent data-[state=open]:text-vexa-accent-foreground\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto size-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(menuContentClasses, className)}\n {...props}\n />\n );\n}\n"
}
],
"meta": {
"frameworks": [
"react",
"next"
],
"rsc": "client",
"exports": [
"DropdownMenu",
"DropdownMenuTrigger",
"DropdownMenuContent",
"DropdownMenuGroup",
"DropdownMenuItem",
"DropdownMenuCheckboxItem",
"DropdownMenuRadioGroup",
"DropdownMenuRadioItem",
"DropdownMenuLabel",
"DropdownMenuSeparator",
"DropdownMenuShortcut",
"DropdownMenuSub",
"DropdownMenuSubTrigger",
"DropdownMenuSubContent",
"DropdownMenuPortal"
],
"cssVars": [
"--vexa-border",
"--vexa-surface-raised",
"--vexa-foreground",
"--vexa-muted-foreground",
"--vexa-accent",
"--vexa-accent-foreground"
],
"a11y": "APG menu button: trigger exposes aria-haspopup/aria-expanded; Arrow keys and type-ahead move through items; Escape closes and returns focus; submenus open on ArrowRight.",
"example": "<DropdownMenu><DropdownMenuTrigger asChild><Button variant=\"outline\">Actions</Button></DropdownMenuTrigger><DropdownMenuContent><DropdownMenuItem>Edit</DropdownMenuItem></DropdownMenuContent></DropdownMenu>",
"tests": "planned — see docs/README.md#roadmap"
}
}Detalles
Accesibilidad
APG menu button: trigger exposes aria-haspopup/aria-expanded; Arrow keys and type-ahead move through items; Escape closes and returns focus; submenus open on ArrowRight.
Uso
A menu of actions or commands opened from a button; supports items, checkbox/radio items, submenus and separators.
Anatomía
- DropdownMenu
- Root; owns the open state.
- DropdownMenuTrigger
- The menu button.
- DropdownMenuContent
- The menu surface.
- DropdownMenuItem
- A command item.
- DropdownMenuCheckboxItem / DropdownMenuRadioItem
- Stateful items (with DropdownMenuRadioGroup).
- DropdownMenuLabel / DropdownMenuSeparator
- Section and divide items.
- DropdownMenuShortcut
- Shows a keyboard shortcut hint.
- DropdownMenuSub / DropdownMenuSubTrigger / DropdownMenuSubContent
- A nested submenu.
Accesibilidad
APG menu button: the trigger exposes `aria-haspopup` / `aria-expanded`; menu / menuitem roles inside.
Teclado
| Tecla | Acción |
|---|---|
| Arrow keys / type-ahead | Move through items |
| Enter | Activate the focused item |
| ArrowRight | Open a submenu |
| Escape | Close and return focus to the trigger |
Foco: Focus enters the menu on open and returns to the trigger on close.
Buenas prácticas
Recomendado
- Use it for actions and commands.
- Group items with labels and separators.
- Show shortcuts with DropdownMenuShortcut.
Evitar
- Don't use it as a form Select for values.
- Don't build deep submenu trees.
- Don't put long forms inside the menu.
¿Te ha resultado útil esta página?
Guardado localmente en este navegador. Sin cuenta ni servidor.