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ą.
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
- 1Pole wyszukiwaniaFiltruje listę w trakcie pisania. Fokus zostaje tutaj, a strzałki przesuwają podświetlenie.
- 2Nagłówek grupyOpisuje zestaw powiązanych wyników. Nagłówki zostają na miejscu, gdy ich pozycje są filtrowane.
- 3Podświetlona pozycjaPozycja, którą uruchomi Enter. Podświetlenie podąża za strzałkami i kursorem.
- 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.jsonUż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ę.
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.
"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.
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.
"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.
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ć.
| Klawisz | Dział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ę.
CommandDialogrenderuje wizualnie ukryty tytuł i opis — przekaż sensowne propsytitleidescription. - 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.
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:
CommandInput
CommandItem
CommandGroup
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.