CommandMenu
Hoja clienteA searchable command palette (⌘K).
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/command-menu.jsonSe instala en components/vexa/command-menu.tsx
Dependencias npm
- cmdk
Dependencias del registry
- @vexa/utils
- @vexa/dialog
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 { 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
{
"$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
| Tecla | Acción |
|---|---|
| Type | Filter the list |
| Arrow keys | Move the active option |
| Enter | Run the active item |
| Escape | Close (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.
¿Te ha resultado útil esta página?
Guardado localmente en este navegador. Sin cuenta ni servidor.