Saltar al contenido principal

Search Command

La paleta ⌘K en una sola pieza. Un disparador que parece un input, un diálogo con búsqueda, resultados agrupados, navegación por teclado y una fila de ayudas, cableados a un solo onSelect.

Press ⌘K or click the field

1'use client';
2
3import { useState } from 'react';
4import { SearchCommand, type SearchItem } from '@/components/ui/search-command';
5
6function Glyph({ d }: { d: string }) {
7 return (
8 <svg
9 xmlns="http://www.w3.org/2000/svg"
10 width="16"
11 height="16"
12 viewBox="0 0 24 24"
13 fill="none"
14 stroke="currentColor"
15 strokeWidth="1.5"
16 strokeLinecap="round"
17 strokeLinejoin="round"
18 aria-hidden="true"
19 >
20 <path d={d} />
21 </svg>
22 );
23}
24
25const ICONS = {
26 home: 'M3 11 12 3l9 8v10H3z',
27 file: 'M6 3h8l4 4v14H6z M14 3v4h4',
28 box: 'M4 7l8-4 8 4v10l-8 4-8-4z M4 7l8 4 8-4 M12 11v10',
29 moon: 'M20 14A8 8 0 1 1 10 4a6 6 0 0 0 10 10z',
30 user: 'M20 21a8 8 0 0 0-16 0 M12 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
31};
32
33export function Default() {
34 const [last, setLast] = useState<string | null>(null);
35
36 const items: SearchItem[] = [
37 {
38 id: 'home',
39 label: 'Home',
40 group: 'Pages',
41 icon: <Glyph d={ICONS.home} />,
42 shortcut: ['G', 'H'],
43 },
44 {
45 id: 'docs',
46 label: 'Documentation',
47 group: 'Pages',
48 icon: <Glyph d={ICONS.file} />,
49 shortcut: ['G', 'D'],
50 },
51 {
52 id: 'icons',
53 label: 'Icons',
54 group: 'Pages',
55 icon: <Glyph d={ICONS.box} />,
56 keywords: ['svg', 'glyph'],
57 },
58 {
59 id: 'button',
60 label: 'Button',
61 group: 'Components',
62 description: 'Actions, in five variants',
63 icon: <Glyph d={ICONS.box} />,
64 },
65 {
66 id: 'dialog',
67 label: 'Dialog',
68 group: 'Components',
69 description: 'A modal with focus trapped',
70 icon: <Glyph d={ICONS.box} />,
71 },
72 {
73 id: 'chat',
74 label: 'Chat',
75 group: 'Components',
76 description: 'The whole thread in one piece',
77 icon: <Glyph d={ICONS.box} />,
78 keywords: ['ai', 'hybrid'],
79 },
80 {
81 id: 'theme',
82 label: 'Toggle theme',
83 group: 'Actions',
84 icon: <Glyph d={ICONS.moon} />,
85 shortcut: ['T'],
86 },
87 { id: 'profile', label: 'Open profile', group: 'Actions', icon: <Glyph d={ICONS.user} /> },
88 {
89 id: 'signout',
90 label: 'Sign out',
91 group: 'Actions',
92 icon: <Glyph d={ICONS.user} />,
93 disabled: true,
94 },
95 ];
96
97 return (
98 <div className="flex w-full max-w-sm flex-col gap-3">
99 <SearchCommand
100 items={items}
101 placeholder="Search docs, components, actions…"
102 onSelect={(item) => setLast(item.label)}
103 />
104 <p className="text-muted-foreground text-xs">
105 {last ? `Selected ${last}` : 'Press ⌘K or click the field'}
106 </p>
107 </div>
108 );
109}

Instalación

pnpm dlx nachui add search-command

Anatomía

1import { SearchCommand } from '@/components/ui/search-command';
1<SearchCommand
2 items={[
3 { id: 'docs', label: 'Documentación', group: 'Páginas', href: '/docs' },
4 { id: 'theme', label: 'Cambiar tema', group: 'Acciones', shortcut: ['T'], onSelect: toggle },
5 ]}
6 placeholder="Buscar…"
7 onSelect={(item) => track(item.id)}
8/>
Sin children renderiza el disparador y el diálogo. El disparador parece un campo de búsqueda y muestra el atajo; el diálogo tiene el input, la lista agrupada y la fila de ayudas. Escribir filtra, las flechas mueven el resaltado, Enter selecciona, Escape cierra.
Pasá children para quedarte solo con lo que necesitás. La demo controlada de abajo saca el disparador y abre la paleta desde un botón propio.
1<SearchCommand items={items} open={open} onOpenChange={setOpen} hotkey={null}>
2 <SearchCommand.Dialog />
3</SearchCommand>

Composición

Un hybrid es composición visible. Abrí search-command.tsx y estos son los elements que lo forman, en este orden:
SearchCommand
├── Dialog
│ ├── SearchCommand.Trigger → Dialog.Trigger + Kbd
│ └── SearchCommand.Dialog → Dialog.Content
│ ├── SearchCommand.Input
│ ├── SearchCommand.List
│ │ ├── SearchCommand.Item → Kbd (atajo)
│ │ └── SearchCommand.Empty
│ └── SearchCommand.Footer → Kbd
El diálogo, su overlay, la trampa de foco y Escape vienen de Dialog. Las teclas dibujadas vienen de Kbd. El filtrado y la lista con flechas son del hybrid, chicos como para leerlos de una sentada.

Atajo

hotkey es k por defecto, así que ⌘K en Mac y Ctrl+K en el resto abre y cierra la paleta. El disparador muestra el modificador correcto para la plataforma. Pasá hotkey={null} para no registrar nada y abrirla vos con open.

Items y grupos

Cada item tiene un id y un label. Agregá group y la lista se parte bajo un encabezado chico por grupo, en el orden en que aparece cada uno por primera vez. description va debajo del label, icon a la izquierda, shortcut como teclas a la derecha, y keywords se suman al label y la descripción en la búsqueda. Un item con href y sin onSelect se renderiza como link. disabled lo deja visible pero no se puede elegir.

Filtrado

El filtro por defecto es una coincidencia sin distinguir mayúsculas sobre label, descripción y keywords. Pasá filter para reemplazarlo por el tuyo, difuso o remoto, mientras responda de forma sincrónica sobre los items que ya tenés.

Controlado

open y onOpenChange ponen la paleta bajo tu control, que es lo que querés cuando la abre otra cosa, un dock o un menú.

Nothing selected yet

1'use client';
2
3import { useState } from 'react';
4import { SearchCommand, type SearchItem } from '@/components/ui/search-command';
5
6const ITEMS: SearchItem[] = [
7 { id: 'new', label: 'New file', group: 'Create', shortcut: ['N'] },
8 { id: 'folder', label: 'New folder', group: 'Create' },
9 { id: 'rename', label: 'Rename', group: 'Edit', shortcut: ['F2'] },
10 { id: 'delete', label: 'Move to trash', group: 'Edit', shortcut: ['⌫'] },
11 { id: 'share', label: 'Share link', group: 'Share' },
12];
13
14export function Controlled() {
15 const [open, setOpen] = useState(false);
16 const [last, setLast] = useState<SearchItem | null>(null);
17
18 return (
19 <div className="flex w-full max-w-sm flex-col items-start gap-3">
20 <button
21 type="button"
22 onClick={() => setOpen(true)}
23 className="border-border hover:bg-muted rounded-full border px-3 py-1.5 text-xs transition-colors"
24 >
25 Open command palette
26 </button>
27 <SearchCommand
28 items={ITEMS}
29 open={open}
30 onOpenChange={setOpen}
31 hotkey={null}
32 onSelect={setLast}
33 placeholder="Type a command…"
34 >
35 <SearchCommand.Dialog />
36 </SearchCommand>
37 <p className="text-muted-foreground text-xs">
38 {last ? `Last: ${last.label}` : 'Nothing selected yet'}
39 </p>
40 </div>
41 );
42}

Referencia de API

SearchCommand

PropTipoDefaultDescripción
itemsSearchItem[]-Lo que busca la paleta
openboolean-Estado abierto controlado
defaultOpenbooleanfalseEstado inicial cuando no es controlado
onOpenChange(open: boolean) => void-Se llama cuando cambia el estado
hotkeystring | null'k'Tecla que la abre con ⌘ o Ctrl; null la desactiva
placeholderstringSearch…Texto del disparador y del input
emptyTextstring-Se muestra cuando nada coincide
filter(item: SearchItem, query: string) => boolean-Reemplaza la coincidencia por defecto
onSelect(item: SearchItem) => void-Se llama después del onSelect propio del item
closeOnSelectbooleantrueCierra el diálogo después de elegir
labelsPartial<SearchCommandLabels>InglésPlaceholder, vacío, ayudas y nombre del disparador
classNamestring-Clases CSS adicionales
childrenReactNode-Reemplaza el disparador y el diálogo por defecto

SearchItem

CampoTipoDescripción
idstringClave única, también usada en aria-activedescendant
labelstringEl nombre visible
groupstringEncabezado bajo el que se lista
descriptionstringSegunda línea bajo el label
iconReactNodeSe renderiza a la izquierda
shortcutstring[]Teclas a la derecha
keywordsstring[]Palabras extra sobre las que busca
hrefstringRenderiza el item como link cuando está
onSelect() => voidCorre antes del onSelect de la raíz
disabledbooleanVisible pero no elegible

SearchCommand.Trigger

PropTipoDefaultDescripción
childrenReactNodeplaceholderTexto del campo
classNamestring-Clases CSS adicionales

SearchCommand.Dialog

PropTipoDefaultDescripción
childrenReactNodeInput, List y FooterReemplaza el default
classNamestring-Clases CSS adicionales

SearchCommand.Input

PropTipoDefaultDescripción
classNamestring-Clases CSS adicionales
...--Cualquier prop de input salvo value y onChange

SearchCommand.List

PropTipoDefaultDescripción
classNamestring-Clases CSS adicionales

SearchCommand.Item

PropTipoDefaultDescripción
itemSearchItem-La entrada a renderizar
classNamestring-Clases CSS adicionales

SearchCommand.Empty

PropTipoDefaultDescripción
childrenReactNodeemptyTextQué decir
classNamestring-Clases CSS adicionales

SearchCommand.Footer

PropTipoDefaultDescripción
childrenReactNodeLas tres ayudasReemplaza la fila
classNamestring-Clases CSS adicionales

useSearchCommand

DevuelveDescripción
{ open, setOpen, query, setQuery, results, ... }El estado de la paleta, para partes tuyas
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue