HextaUI

Command

Una lista de acciones con búsqueda, en línea o como paleta ⌘K, con páginas, atajos y coincidencias resaltadas.

import {
  IconCalculator,
  IconCalendar,
  IconCreditCard,
  IconMoodSmile,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandDemo() {
  return (
    <Command highlight className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <IconCalendar />
            Calendar
          </CommandItem>
          <CommandItem>
            <IconMoodSmile />
            Search Emoji
          </CommandItem>
          <CommandItem disabled>
            <IconCalculator />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem shortcut="mod+p">
            <IconUser />
            Profile
          </CommandItem>
          <CommandItem shortcut="mod+b">
            <IconCreditCard />
            Billing
          </CommandItem>
          <CommandItem shortcut="mod+,">
            <IconSettings />
            Settings
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/command.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
<Command>
  <CommandInput placeholder="Type a command or search…" />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Suggestions">
      <CommandItem onSelect={() => openCalendar()}>Calendar</CommandItem>
      <CommandItem shortcut="mod+p">Profile</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

Los atajos usan mod para ⌘ en dispositivos Apple y Ctrl en todos los demás. Las etiquetas se formatean por plataforma automáticamente.

Command
├── CommandInput
├── CommandList
│   ├── CommandEmpty
│   ├── CommandLoading
│   ├── CommandGroup
│   │   └── CommandItem
│   │       └── CommandShortcut
│   ├── CommandSeparator
│   └── CommandPage
│       └── CommandGroup
└── CommandFooter

CommandDialog
└── Command

Básico

Al escribir se filtran y clasifican los elementos sobre la marcha. Los grupos sin coincidencias desaparecen y la altura de la lista se anima para ajustarse a lo que queda.

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandBasic() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search Emoji</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>Profile</CommandItem>
          <CommandItem>Billing</CommandItem>
          <CommandItem>Settings</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Dialog

Pon un <Command /> dentro de <CommandDialog /> y alterna con useCommandHotkey. Pulsa ⌘K o Ctrl K. Los atajos de los elementos funcionan mientras está abierto, las coincidencias se resaltan y preserveSearch conserva la consulta y la selección para la próxima vez que se abra.

"use client"

import * as React from "react"
import {
  IconCalculator,
  IconCalendar,
  IconCheck,
  IconCreditCard,
  IconExternalLink,
  IconMoodSmile,
  IconPalette,
  IconPoint,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandFooter,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
  CommandSeparator,
  CommandShortcut,
  useCommandHotkey,
} from "@/components/ui/command"

export function CommandDialogDemo() {
  const [open, setOpen] = React.useState(false)
  const [last, setLast] = React.useState<string>()
  const [theme, setTheme] = React.useState("System")

  useCommandHotkey("mod+k", () => setOpen((value) => !value))

  const run = (action: string) => {
    setLast(action)
    setOpen(false)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open palette
        <CommandShortcut hotkey="mod+k" />
      </Button>
      <p className="text-sm text-muted-foreground">
        {last ? `Ran “${last}”.` : "Nothing run yet."} Theme: {theme}.
      </p>
      <CommandDialog open={open} onOpenChange={setOpen} preserveSearch>
        <Command highlight>
          <CommandInput placeholder="Type a command or search…" />
          <CommandList>
            <CommandEmpty>
              {(search) => `No results for “${search}”.`}
            </CommandEmpty>
            <CommandGroup heading="Suggestions">
              <CommandItem
                shortcut="mod+shift+c"
                onSelect={() => run("Calendar")}
              >
                <IconCalendar />
                Calendar
              </CommandItem>
              <CommandItem onSelect={() => run("Search Emoji")}>
                <IconMoodSmile />
                Search Emoji
              </CommandItem>
              <CommandItem disabled>
                <IconCalculator />
                Calculator
              </CommandItem>
              <CommandItem page="theme" pageTitle="Theme">
                <IconPalette />
                Change theme…
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Settings">
              <CommandItem shortcut="mod+p" onSelect={() => run("Profile")}>
                <IconUser />
                Profile
              </CommandItem>
              <CommandItem shortcut="mod+b" onSelect={() => run("Billing")}>
                <IconCreditCard />
                Billing
              </CommandItem>
              <CommandItem
                shortcut="mod+,"
                keywords={["preferences", "options"]}
                onSelect={() => run("Settings")}
              >
                <IconSettings />
                Settings
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Links">
              <CommandItem href="/docs" onSelect={() => setOpen(false)}>
                <IconExternalLink />
                All components
              </CommandItem>
            </CommandGroup>
            <CommandPage id="theme">
              <CommandGroup heading="Theme">
                {["Light", "Dark", "System"].map((option) => (
                  <CommandItem
                    key={option}
                    onSelect={() => {
                      setTheme(option)
                      run(`Theme: ${option}`)
                    }}
                  >
                    {option === theme ? <IconCheck /> : <IconPoint />}
                    {option}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandPage>
          </CommandList>
          <CommandFooter />
        </Command>
      </CommandDialog>
    </div>
  )
}

Páginas

Un elemento con page abre el <CommandPage /> correspondiente. El título de la página aparece como un chip en el input, la lista entra deslizándose desde el lado, y Backspace en una búsqueda vacía o Escape vuelve atrás.

"use client"

import * as React from "react"
import {
  IconBrandGithub,
  IconFolder,
  IconFolderPlus,
  IconUsers,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
} from "@/components/ui/command"

const projects = ["hextaui", "marketing-site", "design-tokens"]
const members = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]

export function CommandPages() {
  const [last, setLast] = React.useState<string>()

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <Command>
        <CommandInput placeholder="Search…" />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Workspace">
            <CommandItem page="projects" pageTitle="Projects">
              <IconFolder />
              Projects
            </CommandItem>
            <CommandItem page="members" pageTitle="Members">
              <IconUsers />
              Members
            </CommandItem>
            <CommandItem onSelect={() => setLast("New project")}>
              <IconFolderPlus />
              New project
            </CommandItem>
          </CommandGroup>
          <CommandPage id="projects">
            <CommandGroup heading="Projects">
              {projects.map((project) => (
                <CommandItem key={project} onSelect={setLast}>
                  <IconBrandGithub />
                  {project}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
          <CommandPage id="members">
            <CommandGroup heading="Members">
              {members.map((member) => (
                <CommandItem key={member} onSelect={setLast}>
                  <IconUsers />
                  {member}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
        </CommandList>
      </Command>
      <p className="text-sm text-muted-foreground">
        {last ? `Selected “${last}”.` : "Nothing selected yet."}
      </p>
    </div>
  )
}

Desplazable

Las listas largas se desplazan dentro de una altura limitada. El elemento seleccionado se mantiene siempre a la vista mientras te mueves con el teclado.

import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"

const documents = Array.from({ length: 60 }, (_, index) => ({
  id: index + 1,
  title: `Document ${String(index + 1).padStart(2, "0")}`,
}))

export function CommandScrollable() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Search 60 documents…" />
      <CommandList>
        <CommandEmpty>No documents match.</CommandEmpty>
        <CommandGroup heading="Documents">
          {documents.map((doc) => (
            <CommandItem key={doc.id}>
              <IconFileText />
              {doc.title}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Resultados asíncronos

Define shouldFilter={false} y renderiza los resultados que obtengas. <CommandLoading /> espera 150 ms antes de aparecer y luego permanece al menos 300 ms, así que las respuestas rápidas nunca muestran un spinner por un instante. Prueba ambas latencias.

"use client"

import * as React from "react"
import { IconUser } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandLoading,
  useCommandLoading,
} from "@/components/ui/command"

const people = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Katherine Johnson",
  "Linus Torvalds",
  "Margaret Hamilton",
  "Tim Berners-Lee",
]

export function CommandAsync() {
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState(people)
  const [loading, setLoading] = React.useState(false)
  const [latency, setLatency] = React.useState(700)
  const pending = useCommandLoading(loading)
  const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timerRef.current), [])

  const search = (nextQuery: string, nextLatency: number) => {
    clearTimeout(timerRef.current)
    setLoading(true)
    timerRef.current = setTimeout(() => {
      setResults(
        people.filter((person) =>
          person.toLowerCase().includes(nextQuery.toLowerCase())
        )
      )
      setLoading(false)
    }, nextLatency)
  }

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex gap-2">
        {[80, 700].map((ms) => (
          <Button
            key={ms}
            size="sm"
            variant={latency === ms ? "secondary" : "outline"}
            onClick={() => {
              setLatency(ms)
              search(query, ms)
            }}
          >
            {ms} ms
          </Button>
        ))}
      </div>
      <Command shouldFilter={false} highlight>
        <CommandInput
          placeholder="Search people…"
          value={query}
          onValueChange={(next) => {
            setQuery(next)
            search(next, latency)
          }}
        />
        <CommandList>
          <CommandLoading loading={loading}>Searching…</CommandLoading>
          <CommandEmpty>
            {(value) => `No people match “${value}”.`}
          </CommandEmpty>
          {pending ? null : (
            <CommandGroup heading="People">
              {results.map((person) => (
                <CommandItem key={person}>
                  <IconUser />
                  {person}
                </CommandItem>
              ))}
            </CommandGroup>
          )}
        </CommandList>
      </Command>
    </div>
  )
}

Contenido largo

Los encabezados se ajustan, los nombres largos se truncan o se ajustan según elijas, y los atajos nunca quedan desplazados fuera.

import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandLongContent() {
  return (
    <Command className="w-full max-w-72">
      <CommandInput placeholder="Search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="A group heading that is long enough to wrap onto two lines">
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 truncate">
              quarterly-planning-final-final-v2-reviewed-by-legal.pdf
            </span>
            <CommandShortcut>⌘⇧O</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 wrap-anywhere">
              averyveryverylongunbrokenfilenamethatshouldwrapinsteadofescaping.txt
            </span>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

De derecha a izquierda

Los iconos, los atajos, el chip de página y el deslizamiento de página siguen la dirección de lectura.

import { IconCalendar, IconSettings } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <Command dir="rtl">
        <CommandInput placeholder="ابحث عن أمر…" />
        <CommandList>
          <CommandEmpty>لا توجد نتائج.</CommandEmpty>
          <CommandGroup heading="اقتراحات">
            <CommandItem>
              <IconCalendar />
              التقويم
              <CommandShortcut>⌘T</CommandShortcut>
            </CommandItem>
            <CommandItem>
              <IconSettings />
              الإعدادات
              <CommandShortcut>⌘S</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </div>
  )
}
KeyAcción
↓Selecciona el elemento siguiente.
↑Selecciona el elemento anterior.
Alt↓Salta al primer elemento del grupo siguiente.
Alt↑Salta al primer elemento del grupo anterior.
HomeSelecciona el primer elemento.
EndSelecciona el último elemento.
CtrlNSelecciona el elemento siguiente. Ctrl J también funciona. Desactívalo con vimBindings.
CtrlPSelecciona el elemento anterior. Ctrl K también funciona. Desactívalo con vimBindings.
EnterEjecuta el elemento seleccionado. En un elemento de enlace, ⌘ Enter o Ctrl Enter lo abre en una pestaña nueva.
EscPrimero borra la búsqueda, luego retrocede una página y después cierra el diálogo.
BackspaceRetrocede una página cuando la búsqueda está vacía.
⌘PCualquier atajo de un elemento ejecuta ese elemento mientras el foco está dentro del menú de comandos.
  • El input es un combobox que apunta al elemento seleccionado, así que los lectores de pantalla anuncian cada elemento a medida que te mueves.
  • Una región activa polite anuncia el número de resultados poco después de que dejas de escribir, y anuncia el título de la página cuando abres o sales de una. Cambia el texto con formatResults y rootTitle.
  • <CommandDialog /> tiene un título y una descripción ocultos, atrapa el foco mientras está abierto y lo devuelve al trigger cuando se cierra.
  • Los atajos de los elementos se exponen con aria-keyshortcuts.
  • Con movimiento reducido, los elementos se ejecutan sin el parpadeo de confirmación y las páginas se desvanecen en lugar de deslizarse.

Construido sobre cmdk, con <CommandDialog /> sobre el diálogo de Base UI. Las partes aceptan las props de la parte de cmdk que envuelven.

PropTipoPredeterminado
labelNombre accesible del menú.
string"Command menu"
highlightResalta las letras coincidentes en cada elemento y atenúa el resto.
booleanfalse
shouldFilterPonlo en false para filtrar y ordenar los elementos tú mismo, por ejemplo cuando los resultados vienen de un servidor.
booleantrue
filterDevuelve una puntuación de 0 (oculto) a 1 (mejor coincidencia).
(value: string, search: string, keywords?: string[]) => number–
valueEl valor del elemento seleccionado.
string–
defaultValue
string–
onValueChange
(value: string) => void–
loopVuelve al otro extremo al llegar a los extremos de la lista.
booleanfalse
vimBindingsNavegación con Ctrl N, J, P y K.
booleantrue
disablePointerSelection
booleanfalse
formatResultsTexto que se anuncia a los lectores de pantalla tras escribir.
(count: number) => string"3 results"
rootTitleSe anuncia al salir de la última página y volver a la raíz.
string"All commands"
AtributoDescripción
data-slot="command"Apunta a la raíz en CSS.
data-highlightingPresente mientras highlight está activado y la búsqueda no está vacía.
--command-radiusRadio exterior. Los elementos derivan de él un radio concéntrico.
--command-insetRelleno entre el borde de la lista y sus elementos.
PropTipoPredeterminado
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean, details) => void–
preserveSearchMantiene el diálogo montado para que la consulta, la página y la selección sobrevivan al cierre. La consulta queda seleccionada al reabrirlo.
booleanfalse
titleTítulo del diálogo oculto visualmente.
string"Command menu"
descriptionDescripción del diálogo oculta visualmente.
string"Search for a command to run."
showCloseButton
booleanfalse
classNameSe aplica al popup del diálogo.
string–
AtributoDescripción
data-slot="command-dialog"El popup del diálogo.
data-slot="command-dialog-overlay"El fondo.
data-openPresente en el popup mientras está abierto.
PropTipoPredeterminado
valueTexto de búsqueda controlado.
string–
onValueChange
(search: string) => void–
placeholder
string–
clearLabelNombre accesible del botón de borrar.
string"Clear search"
backLabelNombre accesible del chip de página.
(title: string) => string(title) => `Back from ${title}`
AtributoDescripción
data-slot="command-input"El input.
data-slot="command-input-wrapper"La fila que contiene el icono, el input y el botón de borrar.
data-slot="command-clear"El botón de borrar, que se muestra una vez que escribes.
data-slot="command-page-chip"El chip de retroceso que se muestra en una página.
PropTipoPredeterminado
labelNombre accesible de la lista.
string–
AtributoDescripción
data-slot="command-list"La lista.
data-settledPresente una vez que la lista se ha medido. La transición de altura solo se ejecuta mientras está definido.
--cmdk-list-heightAltura de los elementos visibles, usada para animar la lista.
PropTipoPredeterminado
childrenUsa la forma de función para reflejar la consulta.
ReactNode | (search: string) => ReactNode–
AtributoDescripción
data-slot="command-empty"Oculto mientras hay un CommandLoading en la lista.
PropTipoPredeterminado
loading
booleantrue
delayMilisegundos de espera antes de que aparezca el spinner.
number150
minDurationMilisegundos mínimos que permanece el spinner una vez mostrado.
number300
labelEtiqueta accesible. Por defecto, los hijos de tipo cadena.
string–
progress
number–
AtributoDescripción
data-slot="command-loading"La fila de carga.
data-pendingPresente durante el retraso, mientras la fila se anuncia pero aún no es visible.
PropTipoPredeterminado
heading
ReactNode–
valueObligatorio cuando no hay encabezado.
string–
forceMountMantiene el grupo visible al filtrar.
booleanfalse
AtributoDescripción
data-slot="command-group"El grupo.
[cmdk-group-heading]El elemento de encabezado.
PropTipoPredeterminado
onSelectSe ejecuta al hacer clic, con Enter o con el atajo del elemento, tras el parpadeo de confirmación.
(value: string) => void–
valueSe usa para filtrar. Por defecto es el texto del elemento, sin el atajo.
string–
keywordsPalabras adicionales que coinciden con este elemento.
string[]–
disabled
booleanfalse
shortcutUn atajo como "mod+shift+c". Se muestra en el elemento y lo ejecuta mientras el foco está en el menú.
string–
pageAbre el CommandPage con este id en lugar de ejecutarse.
string–
pageTitleTítulo que se muestra en el chip de página. Por defecto es el valor.
string–
hrefRenderiza el elemento como un enlace. Enter lo sigue, ⌘ o Ctrl Enter abre una pestaña nueva.
string–
renderUn elemento de enlace para renderizar en su lugar, como <Link /> de Next.js.
ReactElement–
confirmHace parpadear brevemente el elemento antes de ejecutarlo, para que se note la elección.
booleantrue
forceMountMantiene el elemento visible al filtrar.
booleanfalse
AtributoDescripción
data-slot="command-item"El elemento.
data-selected="true"Presente en el elemento seleccionado.
data-disabled="true"Presente en los elementos deshabilitados.
data-valueEl valor usado para filtrar.
data-confirmingPresente durante el parpadeo de confirmación.
data-pagePresente en los elementos que abren una página.
PropTipoPredeterminado
idCoincide con la prop page del elemento que lo abre. Sus grupos y elementos solo se renderizan mientras es la página actual.
string–
PropTipoPredeterminado
hotkeyDa formato a un atajo como "mod+k" para la plataforma actual. Los hijos lo reemplazan.
string–
AtributoDescripción
data-slot="command-shortcut"La etiqueta del atajo.
PropTipoPredeterminado
alwaysRenderLo mantiene visible durante la búsqueda.
booleanfalse
AtributoDescripción
data-slot="command-separator"El separador.
PropTipoPredeterminado
childrenPor defecto muestra pistas de teclas que se actualizan en una página. Oculto en pantallas táctiles.
ReactNode–
AtributoDescripción
data-slot="command-footer"El pie.
const [open, setOpen] = React.useState(false)

useCommandHotkey("mod+k", () => setOpen((value) => !value))
PropTipoPredeterminado
hotkeySe escucha en todo el documento. Los atajos sin modificador se ignoran mientras se escribe en un campo.
string–
callback
(event: KeyboardEvent) => void–
options.enabled
booleantrue

Devuelve si debe verse un indicador de carga, con el mismo retraso y duración mínima que <CommandLoading />. Úsalo para ocultar resultados obsoletos mientras hay una petición en curso.

PropTipoPredeterminado
loading
boolean–
options.delay
number150
options.minDuration
number300
  • useCommandPages() devuelve { pages, page, push, pop, reset } para controlar las páginas desde tu propio código.
  • useCommandState(selector) lee el estado de cmdk, como la búsqueda o el número de resultados filtrados.
  • useHotkeyLabel(hotkey) da formato a un atajo para la plataforma actual, como ⌘K o Ctrl+K.

Usado en bloques

Bloques que se construyen sobre Command.