Saltar al contenido
VEXA
Navegación y menús

DropdownMenu

Hoja cliente

A 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.

Escritorio

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.json

Se 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.

components/vexa/dropdown-menu.tsx
"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
r/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

TeclaAcción
Arrow keys / type-aheadMove through items
EnterActivate the focused item
ArrowRightOpen a submenu
EscapeClose 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.
Editar esta página

¿Te ha resultado útil esta página?

Guardado localmente en este navegador. Sin cuenta ni servidor.