Saltar al contenido
VEXA
Navegación y menús

CommandMenu

Hoja cliente

A searchable command palette (⌘K).

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/command-menu.json

Se instala en components/vexa/command-menu.tsx

Dependencias npm

  • cmdk

Dependencias del registry

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/command-menu.tsx
"use client";

import type * as React from "react";
import { Command as CommandPrimitive } from "cmdk";

import { cn } from "@/lib/utils";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogTitle,
} from "@/components/vexa/dialog";

function SearchIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
      <path
        d="m20 20-3.5-3.5"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
      />
    </svg>
  );
}

/**
 * CommandMenu — a searchable command palette (⌘K) built on `cmdk`.
 *
 * Client leaf. `cmdk` provides the combobox interaction model: type to filter,
 * Arrow keys to move the active option, Enter to run it, with the input keeping
 * DOM focus while `aria-activedescendant` tracks the highlighted item. Render
 * `Command` inline, or `CommandDialog` for the overlay palette — the dialog
 * layers `cmdk` on top of the VEXA `Dialog` so focus trapping, Escape, and
 * focus return all apply.
 */
export function Command({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive>) {
  return (
    <CommandPrimitive
      data-slot="command"
      className={cn(
        "flex h-full w-full flex-col overflow-hidden rounded-vexa-md bg-vexa-surface-raised text-vexa-foreground",
        className,
      )}
      {...props}
    />
  );
}

export interface CommandDialogProps extends React.ComponentProps<
  typeof Dialog
> {
  /** Accessible title for the dialog; visually hidden by default. */
  title?: string;
  /** Accessible description for the dialog; visually hidden by default. */
  description?: string;
}

export function CommandDialog({
  title = "Command menu",
  description = "Search for a command to run.",
  children,
  ...props
}: CommandDialogProps) {
  return (
    <Dialog {...props}>
      <DialogContent className="overflow-hidden p-0 shadow-vexa-lg">
        <DialogTitle className="sr-only">{title}</DialogTitle>
        <DialogDescription className="sr-only">{description}</DialogDescription>
        <Command className="[&_[data-slot=command-input-wrapper]]:h-12">
          {children}
        </Command>
      </DialogContent>
    </Dialog>
  );
}

export function CommandInput({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
  return (
    <div
      data-slot="command-input-wrapper"
      className="flex items-center gap-2 border-b border-vexa-border px-3"
    >
      <SearchIcon className="size-4 shrink-0 text-vexa-muted-foreground" />
      <CommandPrimitive.Input
        data-slot="command-input"
        className={cn(
          "flex h-11 w-full rounded-vexa-md bg-transparent py-3 text-sm text-vexa-foreground outline-none",
          "placeholder:text-vexa-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
          className,
        )}
        {...props}
      />
    </div>
  );
}

export function CommandList({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
  return (
    <CommandPrimitive.List
      data-slot="command-list"
      className={cn(
        "max-h-80 overflow-y-auto overflow-x-hidden p-1 scroll-py-1",
        className,
      )}
      {...props}
    />
  );
}

export function CommandEmpty(
  props: React.ComponentProps<typeof CommandPrimitive.Empty>,
) {
  return (
    <CommandPrimitive.Empty
      data-slot="command-empty"
      className="py-6 text-center text-sm text-vexa-muted-foreground"
      {...props}
    />
  );
}

export function CommandGroup({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
  return (
    <CommandPrimitive.Group
      data-slot="command-group"
      className={cn(
        "overflow-hidden p-1 text-vexa-foreground",
        "[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-vexa-muted-foreground",
        className,
      )}
      {...props}
    />
  );
}

export function CommandSeparator({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
  return (
    <CommandPrimitive.Separator
      data-slot="command-separator"
      className={cn("-mx-1 my-1 h-px bg-vexa-border", className)}
      {...props}
    />
  );
}

export function CommandItem({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
  return (
    <CommandPrimitive.Item
      data-slot="command-item"
      className={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)]",
        "data-[selected=true]:bg-vexa-accent data-[selected=true]:text-vexa-accent-foreground",
        "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
        "[&_svg]:size-4 [&_svg]:shrink-0",
        className,
      )}
      {...props}
    />
  );
}

export function CommandShortcut({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="command-shortcut"
      className={cn(
        "ml-auto text-xs tracking-widest text-vexa-muted-foreground",
        className,
      )}
      {...props}
    />
  );
}
JSON del registryr/command-menu.json
r/command-menu.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-menu",
  "type": "registry:ui",
  "title": "CommandMenu",
  "description": "A searchable command palette (⌘K).",
  "dependencies": [
    "cmdk"
  ],
  "registryDependencies": [
    "@vexa/utils",
    "@vexa/dialog"
  ],
  "files": [
    {
      "path": "registry/default/ui/command-menu.tsx",
      "type": "registry:ui",
      "target": "components/vexa/command-menu.tsx",
      "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTitle,\n} from \"@/components/vexa/dialog\";\n\nfunction SearchIcon({ 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      <circle cx=\"11\" cy=\"11\" r=\"7\" stroke=\"currentColor\" strokeWidth=\"2\" />\n      <path\n        d=\"m20 20-3.5-3.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\n/**\n * CommandMenu — a searchable command palette (⌘K) built on `cmdk`.\n *\n * Client leaf. `cmdk` provides the combobox interaction model: type to filter,\n * Arrow keys to move the active option, Enter to run it, with the input keeping\n * DOM focus while `aria-activedescendant` tracks the highlighted item. Render\n * `Command` inline, or `CommandDialog` for the overlay palette — the dialog\n * layers `cmdk` on top of the VEXA `Dialog` so focus trapping, Escape, and\n * focus return all apply.\n */\nexport function Command({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive>) {\n  return (\n    <CommandPrimitive\n      data-slot=\"command\"\n      className={cn(\n        \"flex h-full w-full flex-col overflow-hidden rounded-vexa-md bg-vexa-surface-raised text-vexa-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport interface CommandDialogProps extends React.ComponentProps<\n  typeof Dialog\n> {\n  /** Accessible title for the dialog; visually hidden by default. */\n  title?: string;\n  /** Accessible description for the dialog; visually hidden by default. */\n  description?: string;\n}\n\nexport function CommandDialog({\n  title = \"Command menu\",\n  description = \"Search for a command to run.\",\n  children,\n  ...props\n}: CommandDialogProps) {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-vexa-lg\">\n        <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n        <DialogDescription className=\"sr-only\">{description}</DialogDescription>\n        <Command className=\"[&_[data-slot=command-input-wrapper]]:h-12\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function CommandInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n  return (\n    <div\n      data-slot=\"command-input-wrapper\"\n      className=\"flex items-center gap-2 border-b border-vexa-border px-3\"\n    >\n      <SearchIcon className=\"size-4 shrink-0 text-vexa-muted-foreground\" />\n      <CommandPrimitive.Input\n        data-slot=\"command-input\"\n        className={cn(\n          \"flex h-11 w-full rounded-vexa-md bg-transparent py-3 text-sm text-vexa-foreground outline-none\",\n          \"placeholder:text-vexa-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nexport function CommandList({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n  return (\n    <CommandPrimitive.List\n      data-slot=\"command-list\"\n      className={cn(\n        \"max-h-80 overflow-y-auto overflow-x-hidden p-1 scroll-py-1\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CommandEmpty(\n  props: React.ComponentProps<typeof CommandPrimitive.Empty>,\n) {\n  return (\n    <CommandPrimitive.Empty\n      data-slot=\"command-empty\"\n      className=\"py-6 text-center text-sm text-vexa-muted-foreground\"\n      {...props}\n    />\n  );\n}\n\nexport function CommandGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n  return (\n    <CommandPrimitive.Group\n      data-slot=\"command-group\"\n      className={cn(\n        \"overflow-hidden p-1 text-vexa-foreground\",\n        \"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-vexa-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CommandSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n  return (\n    <CommandPrimitive.Separator\n      data-slot=\"command-separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-vexa-border\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CommandItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n  return (\n    <CommandPrimitive.Item\n      data-slot=\"command-item\"\n      className={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        \"data-[selected=true]:bg-vexa-accent data-[selected=true]:text-vexa-accent-foreground\",\n        \"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\",\n        \"[&_svg]:size-4 [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CommandShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"command-shortcut\"\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-vexa-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
    }
  ],
  "meta": {
    "frameworks": [
      "react",
      "next"
    ],
    "rsc": "client",
    "exports": [
      "Command",
      "CommandDialog",
      "CommandInput",
      "CommandList",
      "CommandEmpty",
      "CommandGroup",
      "CommandItem",
      "CommandShortcut",
      "CommandSeparator"
    ],
    "cssVars": [
      "--vexa-surface-raised",
      "--vexa-foreground",
      "--vexa-muted-foreground",
      "--vexa-border",
      "--vexa-accent",
      "--vexa-accent-foreground"
    ],
    "a11y": "cmdk combobox model: type to filter, Arrow keys move the active option, Enter runs it; the input keeps DOM focus while aria-activedescendant tracks selection. CommandDialog inherits the Dialog focus trap, Escape, and focus return, and carries a visually-hidden title.",
    "example": "<CommandDialog open={open} onOpenChange={setOpen}><CommandInput placeholder=\"Type a command…\" /><CommandList><CommandEmpty>No results.</CommandEmpty><CommandGroup heading=\"Actions\"><CommandItem>New file</CommandItem></CommandGroup></CommandList></CommandDialog>",
    "tests": "planned — see docs/README.md#roadmap"
  }
}

Detalles

Accesibilidad

cmdk combobox model: type to filter, Arrow keys move the active option, Enter runs it; the input keeps DOM focus while aria-activedescendant tracks selection. CommandDialog inherits the Dialog focus trap, Escape, and focus return, and carries a visually-hidden title.

Uso

A searchable command palette / combobox — type to filter a list of commands or items and run one.

Anatomía

Command
Root of the combobox.
CommandInput
The filter input; keeps DOM focus.
CommandList
Scrollable results region.
CommandEmpty
Shown when nothing matches.
CommandGroup
A titled group of items.
CommandItem
A runnable result.
CommandSeparator / CommandShortcut
Divide items and show shortcut hints.
CommandDialog
Wraps Command in a Dialog for a palette.

Accesibilidad

cmdk combobox + listbox: the input keeps DOM focus while `aria-activedescendant` tracks the active option.

Teclado

TeclaAcción
TypeFilter the list
Arrow keysMove the active option
EnterRun the active item
EscapeClose (in CommandDialog)

Foco: The input holds focus; CommandDialog inherits the Dialog focus trap and return and carries a visually-hidden title.

  • Provide a CommandEmpty state and label the input.

Buenas prácticas

Recomendado

  • Provide a CommandEmpty state.
  • Label or place-hold the input.
  • Group results with CommandGroup.

Evitar

  • Don't move DOM focus onto options — use `aria-activedescendant`.
  • Don't ship a CommandDialog without a title.
  • Don't omit the empty state.
Editar esta página

¿Te ha resultado útil esta página?

Guardado localmente en este navegador. Sin cuenta ni servidor.