HextaUI

Command

Une liste d’actions recherchable, intégrée ou en palette ⌘K, avec pages, raccourcis et correspondances surlignées.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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>

Les raccourcis utilisent mod pour ⌘ sur les appareils Apple et Ctrl partout ailleurs. Les libellés sont formatés pour chaque plateforme.

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

CommandDialog
└── Command

Basique

La saisie filtre et classe les éléments au fur et à mesure. Les groupes sans correspondance disparaissent, et la hauteur de la liste s’anime pour s’adapter à ce qui reste.

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

Placez un <Command /> dans <CommandDialog /> et basculez-le avec useCommandHotkey. Appuyez sur ⌘K ou Ctrl K. Les raccourcis d’éléments s’exécutent pendant l’ouverture, les correspondances sont mises en évidence, et preserveSearch conserve la requête et la sélection pour la prochaine ouverture.

"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>
  )
}

Pages

Un élément avec page ouvre le <CommandPage /> correspondant. Le titre de la page apparaît comme une chip dans le champ, la liste glisse depuis le côté, et Backspace dans une recherche vide ou Escape revient en arrière.

"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>
  )
}

Défilant

Les longues listes défilent dans une hauteur plafonnée. L’élément sélectionné reste toujours visible pendant la navigation au clavier.

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>
  )
}

Résultats asynchrones

Définissez shouldFilter={false} et rendez les résultats que vous récupérez. <CommandLoading /> attend 150 ms avant d’apparaître puis reste au moins 300 ms : les réponses rapides n’affichent donc jamais un spinner furtif. Essayez les deux latences.

"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>
  )
}

Contenu long

Les titres passent à la ligne, les noms longs sont tronqués ou passent à la ligne selon votre choix, et les raccourcis ne sont jamais repoussés.

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 droite à gauche

Les icônes, les raccourcis, la chip de page et le glissement des pages suivent tous le sens de lecture.

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>
  )
}
ToucheAction
↓Sélectionne l’élément suivant.
↑Sélectionne l’élément précédent.
Alt↓Saute au premier élément du groupe suivant.
Alt↑Saute au premier élément du groupe précédent.
HomeSélectionne le premier élément.
EndSélectionne le dernier élément.
CtrlNSélectionne l’élément suivant. Ctrl J fonctionne aussi. Désactivez avec vimBindings.
CtrlPSélectionne l’élément précédent. Ctrl K fonctionne aussi. Désactivez avec vimBindings.
EnterExécute l’élément sélectionné. Sur un élément de type lien, ⌘ Enter ou Ctrl Enter l’ouvre dans un nouvel onglet.
EscEfface d’abord la recherche, puis revient d’une page en arrière, puis ferme la boîte de dialogue.
BackspaceRevient d’une page en arrière lorsque la recherche est vide.
⌘PTout raccourci d’élément exécute son élément tant que le focus est dans le menu de commandes.
  • Le champ est un combobox qui pointe vers l’élément sélectionné : les lecteurs d’écran annoncent donc chaque élément au fil du déplacement.
  • Une région live polie annonce le nombre de résultats peu après l’arrêt de la saisie, et annonce le titre de la page à l’ouverture ou à la sortie d’une page. Modifiez la formulation avec formatResults et rootTitle.
  • <CommandDialog /> a un titre et une description masqués, piège le focus pendant l’ouverture et le rend au déclencheur à la fermeture.
  • Les raccourcis d’éléments sont exposés avec aria-keyshortcuts.
  • Avec la réduction des animations, les éléments s’exécutent sans clignotement de confirmation et les pages se fondent au lieu de glisser.

Construit sur cmdk, avec <CommandDialog /> sur la boîte de dialogue de Base UI. Les parties acceptent les props de la partie cmdk qu’elles enveloppent.

PropTypePar défaut
labelNom accessible du menu.
string"Command menu"
highlightMet en évidence les lettres correspondantes dans chaque élément et atténue le reste.
booleanfalse
shouldFilterDéfinissez à false pour filtrer et trier les éléments vous-même, par exemple lorsque les résultats viennent d’un serveur.
booleantrue
filterRetourne un score de 0 (masqué) à 1 (meilleure correspondance).
(value: string, search: string, keywords?: string[]) => number–
valueLa valeur de l’élément sélectionné.
string–
defaultValue
string–
onValueChange
(value: string) => void–
loopReboucle aux extrémités de la liste.
booleanfalse
vimBindingsNavigation avec Ctrl N, J, P et K.
booleantrue
disablePointerSelection
booleanfalse
formatResultsTexte annoncé aux lecteurs d’écran après la saisie.
(count: number) => string"3 results"
rootTitleAnnoncé lorsque vous quittez la dernière page et revenez à la racine.
string"All commands"
AttributDescription
data-slot="command"Ciblez la racine en CSS.
data-highlightingPrésent lorsque highlight est activé et que la recherche n’est pas vide.
--command-radiusRayon extérieur. Les éléments en déduisent un rayon concentrique.
--command-insetRemplissage entre le bord de la liste et ses éléments.
PropTypePar défaut
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean, details) => void–
preserveSearchGarde la boîte de dialogue montée pour que la requête, la page et la sélection survivent à la fermeture. La requête est sélectionnée à la réouverture.
booleanfalse
titleTitre de la boîte de dialogue masqué visuellement.
string"Command menu"
descriptionDescription de la boîte de dialogue masquée visuellement.
string"Search for a command to run."
showCloseButton
booleanfalse
classNameAppliqué à la popup de la boîte de dialogue.
string–
AttributDescription
data-slot="command-dialog"La popup de la boîte de dialogue.
data-slot="command-dialog-overlay"L’arrière-plan.
data-openPrésent sur la popup tant qu’elle est ouverte.
PropTypePar défaut
valueTexte de recherche contrôlé.
string–
onValueChange
(search: string) => void–
placeholder
string–
clearLabelNom accessible du bouton d’effacement.
string"Clear search"
backLabelNom accessible de la chip de page.
(title: string) => string(title) => `Back from ${title}`
AttributDescription
data-slot="command-input"Le champ.
data-slot="command-input-wrapper"La ligne qui contient l’icône, le champ et le bouton d’effacement.
data-slot="command-clear"Le bouton d’effacement, affiché dès que vous saisissez.
data-slot="command-page-chip"La chip de retour affichée sur une page.
PropTypePar défaut
labelNom accessible de la liste.
string–
AttributDescription
data-slot="command-list"La liste.
data-settledPrésent une fois que la liste s’est mesurée. La transition de hauteur ne s’exécute que lorsqu’il est défini.
--cmdk-list-heightHauteur des éléments visibles, utilisée pour animer la liste.
PropTypePar défaut
childrenUtilisez la forme fonction pour répéter la requête.
ReactNode | (search: string) => ReactNode–
AttributDescription
data-slot="command-empty"Masqué tant qu’un CommandLoading est dans la liste.
PropTypePar défaut
loading
booleantrue
delayMillisecondes d’attente avant l’affichage du spinner.
number150
minDurationNombre minimal de millisecondes pendant lesquelles le spinner reste une fois affiché.
number300
labelLibellé accessible. Par défaut, les enfants de type chaîne.
string–
progress
number–
AttributDescription
data-slot="command-loading"La ligne de chargement.
data-pendingPrésent pendant le délai, lorsque la ligne est annoncée mais pas encore visible.
PropTypePar défaut
heading
ReactNode–
valueRequis lorsqu’il n’y a pas de titre.
string–
forceMountGarde le groupe visible pendant le filtrage.
booleanfalse
AttributDescription
data-slot="command-group"Le groupe.
[cmdk-group-heading]L’élément de titre.
PropTypePar défaut
onSelectS’exécute au clic, sur Enter ou avec le raccourci de l’élément, après le clignotement de confirmation.
(value: string) => void–
valueUtilisé pour le filtrage. Par défaut, le texte de l’élément, sans le raccourci.
string–
keywordsMots supplémentaires qui correspondent à cet élément.
string[]–
disabled
booleanfalse
shortcutUn raccourci comme "mod+shift+c". Affiché sur l’élément et l’exécute lorsque le focus est dans le menu.
string–
pageOuvre le CommandPage avec cet id au lieu de s’exécuter.
string–
pageTitleTitre affiché dans la chip de page. Par défaut, la valeur.
string–
hrefRend l’élément comme un lien. Enter le suit, ⌘ ou Ctrl Enter l’ouvre dans un nouvel onglet.
string–
renderUn élément de lien à rendre à la place, comme <Link /> de Next.js.
ReactElement–
confirmFait clignoter brièvement l’élément avant de l’exécuter, pour que le choix soit perceptible.
booleantrue
forceMountGarde l’élément visible pendant le filtrage.
booleanfalse
AttributDescription
data-slot="command-item"L’élément.
data-selected="true"Présent sur l’élément sélectionné.
data-disabled="true"Présent sur les éléments désactivés.
data-valueLa valeur utilisée pour le filtrage.
data-confirmingPrésent pendant le clignotement de confirmation.
data-pagePrésent sur les éléments qui ouvrent une page.
PropTypePar défaut
idCorrespond à la prop page de l’élément qui l’ouvre. Ses groupes et ses éléments ne sont rendus que lorsqu’elle est la page courante.
string–
PropTypePar défaut
hotkeyFormate un raccourci comme "mod+k" pour la plateforme actuelle. Les enfants le remplacent.
string–
AttributDescription
data-slot="command-shortcut"Le libellé du raccourci.
PropTypePar défaut
alwaysRenderLe garde visible pendant la recherche.
booleanfalse
AttributDescription
data-slot="command-separator"Le séparateur.
PropTypePar défaut
childrenPar défaut, des indications de touches qui se mettent à jour selon la page. Masqué sur écran tactile.
ReactNode–
AttributDescription
data-slot="command-footer"Le pied de page.
const [open, setOpen] = React.useState(false)

useCommandHotkey("mod+k", () => setOpen((value) => !value))
PropTypePar défaut
hotkeyÉcouté sur tout le document. Les raccourcis sans modificateur sont ignorés pendant la saisie dans un champ.
string–
callback
(event: KeyboardEvent) => void–
options.enabled
booleantrue

Indique si un indicateur de chargement doit être visible, avec les mêmes délai et durée minimale que <CommandLoading />. Utilisez-le pour masquer les résultats périmés pendant qu’une requête est en cours.

PropTypePar défaut
loading
boolean–
options.delay
number150
options.minDuration
number300
  • useCommandPages() retourne { pages, page, push, pop, reset } pour piloter les pages depuis votre propre code.
  • useCommandState(selector) lit l’état de cmdk, comme la recherche ou le nombre d’éléments filtrés.
  • useHotkeyLabel(hotkey) formate un raccourci pour la plateforme actuelle, comme ⌘K ou Ctrl+K.

Utilisé dans les blocks

Des blocks qui s’appuient sur Command.