Command
Componente Command de shadcn - paleta de comandos, búsqueda y navegación con teclado impulsada por cmdk.
Busca en todas las páginas de la documentación
Componente Command de shadcn - paleta de comandos, búsqueda y navegación con teclado impulsada por cmdk.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
npx shadcn@latest add command dialogimport {
Command, CommandDialog, CommandEmpty, CommandGroup,
CommandInput, CommandItem, CommandList, CommandSeparator,
} from "@/components/ui/command";
// Menú de comandos en línea
<Command>
<CommandInput placeholder="Buscar..." />
<CommandList>
<CommandEmpty>No se encontraron resultados.</CommandEmpty>
<CommandGroup heading="Acciones">
<CommandItem onSelect={() => console.log("new")}>Archivo nuevo</CommandItem>
<CommandItem onSelect={() => console.log("save")}>Guardar</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Configuración">
<CommandItem>Perfil</CommandItem>
<CommandItem>Preferencias</CommandItem>
</CommandGroup>
</CommandList>
</Command>Cuándo usarlo: Cuando quieres una paleta de búsqueda o acciones prioritaria para el teclado - navegación estilo Cmd+K, búsqueda tipo spotlight o selección con combobox.
"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import {
CommandDialog, CommandEmpty, CommandGroup,
CommandInput, CommandItem, CommandList, CommandSeparator,
} from "@/components/ui/command";
import {
FileText, Settings, User, Search, Plus, Moon, Sun, LogOut,
} from "lucide-react";
type CommandAction = {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
};
export function CommandPalette() {
const [open, setOpen] = useState(false);
const router = useRouter();
// Cmd+K para abrir
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
const runAndClose = useCallback(
(fn: () => void) => {
fn();
setOpen(false);
},
[]
);
const pages: CommandAction[] = [
{ id: "home", label: "Inicio", icon: <FileText className="mr-2 h-4 w-4" />, action: () => router.push("/") },
{ id: "dashboard", label: "Panel", icon: <FileText className="mr-2 h-4 w-4" />, action: () => router.push("/dashboard") },
{ id: "settings", label: "Configuración", icon: <Settings className="mr-2 h-4 w-4" />, shortcut: "Cmd+,", action: () => router.push("/settings") },
{ id: "profile", label: "Perfil", icon: <User className="mr-2 h-4 w-4" />, action: () => router.push("/profile") },
];
const actions: CommandAction[] = [
{ id: "new-doc", label: "Documento nuevo", icon: <Plus className="mr-2 h-4 w-4" />, shortcut: "Cmd+N", action: () => console.log("new doc") },
{ id: "search", label: "Buscar en todo", icon: <Search className="mr-2 h-4 w-4" />, shortcut: "Cmd+F", action: () => console.log("search") },
];
const theme: CommandAction[] = [
{ id: "light", label: "Modo claro", icon: <Sun className="mr-2 h-4 w-4" />, action: () => document.documentElement.classList.remove("dark") },
{ id: "dark", label: "Modo oscuro", icon: <Moon className="mr-2 h-4 w-4" />, action: () => document.documentElement.classList.add("dark") },
];
return (
<>
{/* Botón de activación */}
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted"
>
<Search className="h-4 w-4" />
<span>Buscar...</span>
<kbd className="ml-4 rounded bg-muted px-1.5 py-0.5 text-xs font-mono">Cmd+K</kbd>
</button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Escribe un comando o busca..." />
<CommandList>
<CommandEmpty>No se encontraron resultados.</CommandEmpty>
<CommandGroup heading="Páginas">
{pages.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
{item.shortcut && (
<kbd className="ml-auto text-xs text-muted-foreground">{item.shortcut}</kbd>
)}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Acciones">
{actions.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
{item.shortcut && (
<kbd className="ml-auto text-xs text-muted-foreground">{item.shortcut}</kbd>
)}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Tema">
{theme.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Cuenta">
<CommandItem onSelect={() => runAndClose(() => console.log("logout"))}>
<LogOut className="mr-2 h-4 w-4" />
<span>Cerrar sesión</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}Qué demuestra esto:
Cmd+K para abrirCommandDialog (renderiza en un Dialog)onSelect que navegan o disparan accionescmdk de Paco Coursey - un menú de comandos composable headlessCommandInput proporciona un cuadro de búsqueda que filtra automáticamente los hijos CommandItem por contenido de textoCommandItem es navegable con teclado - las flechas se mueven entre ítems, Enter seleccionaCommandDialog combina Command con el Dialog de shadcn para la superposición modalCommandGroup proporciona agrupación visual con un encabezado opcionalCommandEmpty se renderiza cuando ningún ítem coincide con la consulta de búsquedaBúsqueda asíncrona con carga:
function AsyncCommand() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) { setResults([]); return; }
setLoading(true);
const timer = setTimeout(async () => {
const data = await fetch(`/api/search?q=${query}`).then((r) => r.json());
setResults(data);
setLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<Command shouldFilter={false}>
<CommandInput value={query} onValueChange={setQuery} />
<CommandList>
{loading && <CommandEmpty>Buscando...</CommandEmpty>}
{!loading && results.length === 0 && <CommandEmpty>Sin resultados.</CommandEmpty>}
{results.map((r) => (
<CommandItem key={r.id} value={r.id} onSelect={() => navigate(r)}>
{r.title}
</CommandItem>
))}
</CommandList>
</Command>
);
}Combobox (select con búsqueda):
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
function Combobox({ options, value, onSelect }: {
options: { value: string; label: string }[];
value: string;
onSelect: (v: string) => void;
}) {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open}>
{options.find((o) => o.value === value)?.label ?? "Seleccionar..."}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Buscar..." />
<CommandList>
<CommandEmpty>Sin coincidencias.</CommandEmpty>
<CommandGroup>
{options.map((opt) => (
<CommandItem key={opt.value} value={opt.value} onSelect={() => { onSelect(opt.value); setOpen(false); }}>
{opt.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}// onSelect de CommandItem recibe el string value en minúsculas
<CommandItem
value="unique-id"
onSelect={(value: string) => {
// value es la versión en minúsculas del texto del ítem o de la prop value explícita
}}
>
// Tipado de acciones de comando
interface CommandAction {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
keywords?: string[]; // términos de búsqueda adicionales
}El value de onSelect está en minúsculas - cmdk convierte el value a minúsculas para la coincidencia. Solución: Usa una prop value separada y busca los datos originales por ID.
shouldFilter={false} para asíncrono - Si obtienes resultados de una API, desactiva el filtro integrado. Solución: Establece shouldFilter={false} y maneja el filtrado en el servidor.
Los ítems deben tener contenido de texto - cmdk filtra por el contenido de texto de CommandItem. Si tu ítem solo tiene iconos, el filtrado no funcionará. Solución: Añade texto visible o usa la prop value para filtrado personalizado.
z-index del Dialog - CommandDialog usa el mismo z-index que Dialog. Si tienes otras superposiciones, pueden entrar en conflicto. Solución: Ajusta el z-index mediante className en CommandDialog.
Conflictos de atajos de teclado - Cmd+K puede entrar en conflicto con atajos del navegador o del editor. Solución: Comprueba conflictos y documenta el atajo para los usuarios.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
| cmdk (bare) | Quieres la librería headless sin estilos de shadcn | Ya usas shadcn |
| Algolia DocSearch | Necesitas búsqueda de texto completo en documentación | Necesitas una paleta de comandos general |
| kbar | Quieres una paleta de comandos alternativa con otra API | cmdk cubre tus necesidades |
<datalist> nativo | Necesitas un autocompletado simple nativo del navegador | Quieres navegación rica con teclado y agrupación |
Command renderiza un menú de comandos en línea incrustado en la páginaCommandDialog envuelve Command dentro de un Dialog de shadcn para una superposición modalCommandDialog para paletas estilo Cmd+K; usa Command para comboboxes en líneauseEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);cmdk de Paco Coursey - una librería de menú de comandos composable headlessvalue separada en CommandItem y busca los datos originales por ID en lugar de depender del string de onSelectCommand dentro de un Popover en lugar de un DialogPopoverTrigger como botón y PopoverContent conteniendo el CommandsetOpen(false)CommandItemuseEffect(() => {
if (!query) { setResults([]); return; }
setLoading(true);
const timer = setTimeout(async () => {
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data);
setLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [query]);CommandItem coincide con la consulta de búsqueda actualinterface CommandAction {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
keywords?: string[];
}CommandItemvalue explícitamente para filtrado personalizadoconst runAndClose = useCallback((fn: () => void) => {
fn();
setOpen(false);
}, []);
<CommandItem onSelect={() => runAndClose(item.action)}>onSelect recibe un callback de tipo (value: string) => voidvalue es el contenido de texto en minúsculas o la prop value explícitaRevisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥