Przejdź do treści

Command

Szybka, projektowana z myślą o klawiaturze lista poleceń i wyników z wyszukiwaniem rozmytym — osadzona w stronie albo jako paleta ⌘K nad jej treścią.

Kod źródłowycmdkRejestr
Brak wyników.
import {
  CalculatorIcon,
  CalendarIcon,
  CreditCardIcon,
  SettingsIcon,
  SmileIcon,
  UserIcon,
} from "lucide-react"

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

export default function CommandDemo() {
  return (
    <Command className="w-full max-w-md rounded-xl border shadow-floating">
      <CommandInput placeholder="Wpisz polecenie lub wyszukaj…" />
      <CommandList>
        <CommandEmpty>Brak wyników.</CommandEmpty>
        <CommandGroup heading="Sugestie">
          <CommandItem>
            <CalendarIcon />
            <span>Kalendarz</span>
          </CommandItem>
          <CommandItem>
            <SmileIcon />
            <span>Szukaj emoji</span>
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            <span>Kalkulator</span>
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Ustawienia">
          <CommandItem>
            <UserIcon />
            <span>Profil</span>
            <CommandShortcut>⌘P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            <span>Płatności</span>
            <CommandShortcut>⌘B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <SettingsIcon />
            <span>Ustawienia</span>
            <CommandShortcut>⌘S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Anatomia

  1. 1Pole wyszukiwaniaFiltruje listę w trakcie pisania. Fokus zostaje tutaj, a strzałki przesuwają podświetlenie.
  2. 2Nagłówek grupyOpisuje zestaw powiązanych wyników. Nagłówki zostają na miejscu, gdy ich pozycje są filtrowane.
  3. 3Podświetlona pozycjaPozycja, którą uruchomi Enter. Podświetlenie podąża za strzałkami i kursorem.
  4. 4SkrótKlawisze, które uruchamiają pozycję bez otwierania palety. Pełni wyłącznie funkcję informacyjną.

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/command.json

Użycie

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} 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 onSelect={() => openEmoji()}>Search emoji</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

Command opiera się na cmdk. W trakcie pisania filtruje pozycje i układa je według trafności, zawsze trzyma jedną z nich podświetloną i uruchamia jej onSelect po naciśnięciu Enter lub kliknięciu. Gdy zmieniają się wyniki, lista płynnie animuje swoją wysokość, więc paleta nigdy nie skacze.

Przykłady

Osadzony w treści

Renderuj Command bezpośrednio, gdy selektor albo launcher ma być osadzony w stronie. Grupy ułatwiają przeglądanie wyników, a CommandShortcut podpowiada szybszą drogę.

Brak wyników.
import {
  CalculatorIcon,
  CalendarIcon,
  CreditCardIcon,
  SettingsIcon,
  SmileIcon,
  UserIcon,
} from "lucide-react"

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

export default function CommandDemo() {
  return (
    <Command className="w-full max-w-md rounded-xl border shadow-floating">
      <CommandInput placeholder="Wpisz polecenie lub wyszukaj…" />
      <CommandList>
        <CommandEmpty>Brak wyników.</CommandEmpty>
        <CommandGroup heading="Sugestie">
          <CommandItem>
            <CalendarIcon />
            <span>Kalendarz</span>
          </CommandItem>
          <CommandItem>
            <SmileIcon />
            <span>Szukaj emoji</span>
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            <span>Kalkulator</span>
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Ustawienia">
          <CommandItem>
            <UserIcon />
            <span>Profil</span>
            <CommandShortcut>⌘P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            <span>Płatności</span>
            <CommandShortcut>⌘B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <SettingsIcon />
            <span>Ustawienia</span>
            <CommandShortcut>⌘S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Paleta poleceń

CommandDialog umieszcza listę poleceń w oknie Dialog zakotwiczonym wysoko na ekranie, więc wyniki rosną w dół, a pole wyszukiwania się nie przesuwa. Przypisz paletę do skrótu ⌘ K (ten przykład używa ⌘ J, żeby nie kolidować z wyszukiwarką tej strony, zbudowaną z tego samego komponentu) i dodaj CommandFooter z podpowiedziami klawiszy.

"use client"

import * as React from "react"
import {
  FileTextIcon,
  FolderPlusIcon,
  LayoutDashboardIcon,
  MoonIcon,
  SearchIcon,
  SettingsIcon,
  UserPlusIcon,
} from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  CommandDialog,
  CommandEmpty,
  CommandFooter,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"

export default function CommandDialogDemo() {
  const [open, setOpen] = React.useState(false)

  React.useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "j" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault()
        setOpen((value) => !value)
      }
    }
    document.addEventListener("keydown", onKeyDown)
    return () => document.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <>
      <Button
        variant="outline"
        className="w-64 justify-start pr-1.5 font-normal text-muted-foreground"
        onClick={() => setOpen(true)}
      >
        <SearchIcon data-icon="inline-start" />
        <span className="flex-1 text-left">Szukaj lub przejdź do…</span>
        <KbdGroup>
          <Kbd>⌘</Kbd>
          <Kbd>J</Kbd>
        </KbdGroup>
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <CommandInput placeholder="Szukaj projektów, osób i akcji…" />
        <CommandList>
          <CommandEmpty>Brak wyników.</CommandEmpty>
          <CommandGroup heading="Nawigacja">
            <CommandItem onSelect={() => setOpen(false)}>
              <LayoutDashboardIcon />
              Pulpit
              <CommandShortcut>G D</CommandShortcut>
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <FileTextIcon />
              Dokumenty
              <CommandShortcut>G F</CommandShortcut>
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <SettingsIcon />
              Ustawienia
              <CommandShortcut>G S</CommandShortcut>
            </CommandItem>
          </CommandGroup>
          <CommandSeparator />
          <CommandGroup heading="Akcje">
            <CommandItem onSelect={() => setOpen(false)}>
              <FolderPlusIcon />
              Utwórz projekt…
              <CommandShortcut>⌘N</CommandShortcut>
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <UserPlusIcon />
              Zaproś do zespołu…
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <MoonIcon />
              Przełącz motyw
              <CommandShortcut>D</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
        <CommandFooter />
      </CommandDialog>
    </>
  )
}
const [open, setOpen] = React.useState(false)

React.useEffect(() => {
  const onKeyDown = (event: KeyboardEvent) => {
    if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
      event.preventDefault()
      setOpen((value) => !value)
    }
  }
  document.addEventListener("keydown", onKeyDown)
  return () => document.removeEventListener("keydown", onKeyDown)
}, [])

return (
  <CommandDialog open={open} onOpenChange={setOpen}>
    <CommandInput placeholder="Search…" />
    <CommandList>{/* groups and items */}</CommandList>
    <CommandFooter />
  </CommandDialog>
)

Stan pusty

CommandEmpty renderuje się tylko wtedy, gdy nic nie pasuje. Zadbaj, żeby był przydatny: powiedz, czego szukano, i zaproponuj, co zrobić dalej.

Nie znaleziono repozytoriówSprawdź pisownię albo szukaj we wszystkich organizacjach z prefiksem org:
"use client"

import * as React from "react"
import { SearchXIcon } from "lucide-react"

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

const repositories = [
  "prfct/ui",
  "prfct/tokens",
  "prfct/docs",
  "prfct/registry",
]

export default function CommandEmptyState() {
  const [search, setSearch] = React.useState("figma")

  return (
    <Command className="w-full max-w-md rounded-xl border shadow-floating">
      <CommandInput
        placeholder="Szukaj repozytoriów…"
        value={search}
        onValueChange={setSearch}
      />
      <CommandList>
        <CommandEmpty className="flex flex-col items-center gap-2 py-10">
          <span className="flex size-10 items-center justify-center rounded-xl border bg-surface text-muted-foreground">
            <SearchXIcon className="size-5" />
          </span>
          <span className="text-sm font-medium text-foreground">
            Nie znaleziono repozytoriów
          </span>
          <span className="max-w-60 text-body-xs text-muted-foreground">
            Sprawdź pisownię albo szukaj we wszystkich organizacjach z prefiksem{" "}
            <span className="font-mono">org:</span>
          </span>
        </CommandEmpty>
        <CommandGroup heading="Repozytoria">
          {repositories.map((repo) => (
            <CommandItem key={repo}>{repo}</CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Słowa kluczowe i aliasy

Użytkownicy szukają własnymi słowami. Dodaj keywords, żeby tryb ciemny znajdował Wygląd, a faktura — Płatności. Słowa kluczowe liczą się przy dopasowaniu, ale nie są wyświetlane.

Brak pasujących ustawień.
import {
  CreditCardIcon,
  KeyRoundIcon,
  LanguagesIcon,
  PaletteIcon,
  ShieldCheckIcon,
} from "lucide-react"

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

const settings = [
  {
    label: "Wygląd",
    icon: PaletteIcon,
    keywords: ["motyw", "tryb ciemny", "kolory"],
  },
  {
    label: "Język i region",
    icon: LanguagesIcon,
    keywords: ["ustawienia regionalne", "strefa czasowa", "tłumaczenie"],
  },
  {
    label: "Płatności",
    icon: CreditCardIcon,
    keywords: ["faktura", "płatność", "plan", "subskrypcja"],
  },
  {
    label: "Bezpieczeństwo",
    icon: ShieldCheckIcon,
    keywords: ["2fa", "hasło", "sesje"],
  },
  {
    label: "Klucze API",
    icon: KeyRoundIcon,
    keywords: ["token", "programista", "sekret"],
  },
]

export default function CommandKeywords() {
  return (
    <Command className="w-full max-w-sm rounded-xl border shadow-floating">
      <CommandInput placeholder="Wpisz np. „tryb ciemny” lub „faktura”…" />
      <CommandList>
        <CommandEmpty>Brak pasujących ustawień.</CommandEmpty>
        <CommandGroup heading="Ustawienia">
          {settings.map(({ label, icon: Icon, keywords }) => (
            <CommandItem key={label} keywords={keywords}>
              <Icon />
              {label}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Wybór wielokrotny

W selektorach, w których można zaznaczyć kilka wartości, nie zamykaj listy po wybraniu pozycji i ustawiaj na pozycjach data-checked — prfct wyświetli na ich końcu znacznik wyboru.

Nie znaleziono etykiet.
"use client"

import * as React from "react"

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

const labels = [
  { value: "bug", label: "Błąd", color: "var(--red-9)" },
  { value: "feature", label: "Funkcja", color: "var(--brand-9)" },
  { value: "design", label: "Projektowanie", color: "var(--violet-9)" },
  { value: "docs", label: "Dokumentacja", color: "var(--teal-9)" },
  { value: "perf", label: "Wydajność", color: "var(--amber-9)" },
]

export default function CommandLabels() {
  const [selected, setSelected] = React.useState<string[]>([
    "feature",
    "design",
  ])

  function toggle(value: string) {
    setSelected((current) =>
      current.includes(value)
        ? current.filter((item) => item !== value)
        : [...current, value]
    )
  }

  return (
    <Command className="w-full max-w-xs rounded-xl border shadow-floating">
      <CommandInput placeholder="Dodaj etykiety…" />
      <CommandList>
        <CommandEmpty>Nie znaleziono etykiet.</CommandEmpty>
        <CommandGroup heading={`Wybrano: ${selected.length}`}>
          {labels.map((label) => (
            <CommandItem
              key={label.value}
              value={label.label}
              data-checked={selected.includes(label.value)}
              onSelect={() => toggle(label.value)}
            >
              <span
                aria-hidden="true"
                className="size-2.5 shrink-0 rounded-full"
                style={{ backgroundColor: label.color }}
              />
              {label.label}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Wyniki asynchroniczne

Gdy wyniki przychodzą z serwera, wyłącz wbudowane filtrowanie przez shouldFilter={false}, steruj wyszukiwaniem za pomocą value i onValueChange na CommandInput i renderuj pobrane pozycje.

<Command shouldFilter={false}>
  <CommandInput value={query} onValueChange={setQuery} />
  <CommandList>
    {isLoading && <CommandEmpty>Searching…</CommandEmpty>}
    {results.map((result) => (
      <CommandItem key={result.id} value={result.id} onSelect={open}>
        {result.title}
      </CommandItem>
    ))}
  </CommandList>
</Command>

Wytyczne

Kiedy używać

  • Jako globalna paleta (⌘ K) do nawigacji i akcji w aplikacjach, które mają więcej niż kilka ekranów.
  • Jako osadzona lista z wyszukiwaniem tam, gdzie Select już nie wystarcza: przypisane osoby, etykiety, emoji, pliki.

Kiedy nie używać

  • Do wyboru pojedynczej wartości w formularzu — kontrolką formularza jest Combobox, z widoczną wartością, walidacją i etykietą.
  • Do krótkich, stałych list akcji — Dropdown Menu szybciej się przegląda, niż przeszukuje.
  • Gdy paleta miałaby być jedyną drogą do polecenia. Paleta to skrót; wszystko, co w niej jest, powinno istnieć także w interfejsie.

Nazywanie pozycji

Nazywaj pozycje tak, jak ludzie ich szukają: miejsca docelowe rzeczownikami (Ustawienia, Płatności), akcje wyrażeniami z czasownikiem (Utwórz projekt…, Zaproś do zespołu…). Wielokropek dodawaj do pozycji, które wymagają dalszych danych. Grupuj wyniki według rodzaju — Nawigacja, Akcje, Ostatnie — a najbardziej prawdopodobne trzymaj w pierwszej grupie.

Akcje
Utwórz projekt… ⌘N
Zaproś do zespołu…
DobrzeKonkretne, łatwe do wyszukania etykiety pogrupowane według rodzaju, ze skrótem, który następnym razem pozwoli pominąć paletę.
Nowy
Sprawy z ludźmi
Różne
ŹleMgliste etykiety, które nie pasują do niczego, co ludzie faktycznie wpisują.

Kolejność wyników

W trakcie pisania pozycje są szeregowane w obrębie swojej grupy — najpierw dopasowania od początku słowa i całe słowa, na końcu litery rozrzucone po etykiecie — ale grupy zachowują kolejność, w jakiej je renderujesz. Na początku umieść grupę, po którą ludzie sięgają najczęściej. W palecie, która przeszukuje wiele sekcji, np. dokumentację, renderuj wyniki jako jedną grupę, dopóki zapytanie nie jest puste — wtedy podświetlone jest zawsze najlepsze trafienie. Ogranicz keywords do kilku słów: przy długich opisach pasuje niemal każde zapytanie.

Wydajność

cmdk bez trudu radzi sobie z kilkoma tysiącami pozycji. Przy większej liczbie filtruj na serwerze (zobacz Wyniki asynchroniczne) albo ogranicz liczbę renderowanych wyników — lista nie jest wirtualizowana.

Dostępność

Command implementuje wzorzec WAI-ARIA combobox: pole ma rolę combobox i steruje listą listbox, a podświetlona opcja (option) jest ogłaszana przez aria-activedescendant, podczas gdy fokus zostaje w polu — dzięki temu można jednocześnie pisać i nawigować.

KlawiszDziałanie
↓↑
Podświetla następną lub poprzednią pozycję. Z ustawionym loop przechodzi z końca listy na początek i odwrotnie.
HomeEnd
Podświetla pierwszą lub ostatnią pozycję.
⌘↓⌘↑
Przeskakuje do ostatniej lub pierwszej pozycji.
Alt↓Alt↑
Przeskakuje do pierwszej pozycji następnej lub poprzedniej grupy.
CtrlNCtrlJ
Podświetla następną pozycję (wyłączysz to przez vimBindings={false}).
CtrlPCtrlK
Podświetla poprzednią pozycję.
Enter
Wywołuje onSelect podświetlonej pozycji.
Esc
W CommandDialog zamyka paletę i przywraca fokus do elementu, który ją otworzył.
  • Nazwij paletę. CommandDialog renderuje wizualnie ukryty tytuł i opis — przekaż sensowne propsy title i description.
  • Wyłączone pozycje są pomijane podczas nawigacji i ogłaszane jako niedostępne.
  • Ikony w pozycjach są dekoracyjne; ogłaszany i dopasowywany jest tekst pozycji.

Dokumentacja API

Command

Komponent główny z cmdk.

PropTypDomyślnie
label

Dostępna etykieta menu poleceń.

stringBrak wartości domyślnej
shouldFilter

Ustaw false, żeby samodzielnie filtrować i sortować pozycje, np. wyniki z serwera.

booleantrue
filter

Własna funkcja oceny trafności. Zwróć 0, żeby ukryć pozycję, i 1 dla idealnego dopasowania.

(value: string, search: string, keywords?: string[]) => numberBrak wartości domyślnej
value

Kontrolowana wartość podświetlonej pozycji.

stringBrak wartości domyślnej
defaultValue

Początkowo podświetlona pozycja w trybie niekontrolowanym.

stringBrak wartości domyślnej
onValueChange

Wywoływana, gdy zmienia się podświetlona pozycja.

(value: string) => voidBrak wartości domyślnej
loop

Czy strzałki przechodzą z ostatniej pozycji na pierwszą.

booleanfalse
vimBindings

Włącza nawigację skrótami Ctrl+N/J/P/K.

booleantrue

CommandDialog

Command wewnątrz okna Dialog. Umieść CommandInput, CommandList i CommandFooter bezpośrednio w środku — okno samo dostarcza komponent główny Command (jeśli przekażesz własny Command, to on posłuży jako komponent główny). Przyjmuje propsy komponentu głównego Dialog (open, onOpenChange, …), a ponadto:

PropTypDomyślnie
title

Wizualnie ukryty tytuł okna dla technologii wspomagających. Domyślnie komunikat bieżącego języka.

stringmessages.commandPalette
description

Wizualnie ukryty opis okna. Domyślnie komunikat bieżącego języka.

stringmessages.commandDescription
showCloseButton

Pokazuje przycisk zamykania okna.

booleanfalse
className

Klasy dla powierzchni okna.

stringBrak wartości domyślnej
commandProps

Propsy wewnętrznego komponentu głównego Command, np. filter, shouldFilter, loop albo kontrolowane value.

ComponentProps<typeof Command>Brak wartości domyślnej

CommandInput

PropTypDomyślnie
value

Kontrolowany tekst wyszukiwania.

stringBrak wartości domyślnej
onValueChange

Wywoływana przy każdej zmianie wyszukiwanego tekstu.

(search: string) => voidBrak wartości domyślnej
placeholder

Podpowiedź widoczna, gdy pole jest puste.

stringBrak wartości domyślnej

CommandItem

PropTypDomyślnie
onSelect

Wywoływana po naciśnięciu Enter lub kliknięciu.

(value: string) => voidBrak wartości domyślnej
value

Wartość używana do filtrowania i wyboru. Domyślnie treść tekstowa pozycji.

stringBrak wartości domyślnej
keywords

Dodatkowe terminy, do których pozycja pasuje, choć nie są wyświetlane.

string[]Brak wartości domyślnej
disabled

Pomija pozycję podczas nawigacji i ignoruje jej wybór.

booleanfalse
forceMount

Renderuje pozycję zawsze, niezależnie od wyszukiwania.

booleanfalse
data-checked

Pokazuje znacznik wyboru na końcu pozycji w listach wielokrotnego wyboru.

booleanBrak wartości domyślnej

CommandGroup

PropTypDomyślnie
heading

Nagłówek wyświetlany nad grupą i używany jako jej dostępna nazwa.

ReactNodeBrak wartości domyślnej
value

Wymagany, gdy grupa nie ma nagłówka; musi być unikalny.

stringBrak wartości domyślnej
forceMount

Renderuje grupę zawsze, nawet gdy żadna z jej pozycji nie pasuje.

booleanfalse

CommandList, CommandEmpty, CommandSeparator, CommandShortcut, CommandFooter

CommandList to przewijany obszar wyników; jego wysokość nie przekracza min(24rem, 60dvh) i jest animowana. CommandEmpty renderuje się, gdy nie ma wyników: to <div>, który przyjmuje wszystkie propsy elementu div i jest prezentowany jako wyłączona opcja (option), więc lista listbox nigdy nie jest pusta, a czytnik ekranu odczytuje twój komunikat. CommandSeparator przyjmuje alwaysRender, żeby pozostać widocznym podczas wyszukiwania. CommandShortcut to czysto prezentacyjna podpowiedź na końcu pozycji. CommandFooter domyślnie renderuje podpowiedzi nawigacji — przekaż własne elementy potomne, żeby je zastąpić. Poniżej breakpointu sm jest ukryty, bo telefon nie ma klawiatury, do której podpowiedzi by się odnosiły.