KDS
Overlays

Command

Paleta de comandos teclado-first — ⌘K floating ou inline (Combobox).

Overview

Command é uma paleta de comandos construída sobre cmdk. Tem dois modos canônicos:

  • CommandDialog — paleta flutuante ⌘K (GitHub, Linear, Vercel, Notion). Aberto por atalho de teclado, foco no input automático.
  • Command inline — usado dentro de Popover para o padrão Combobox (input com busca + lista filtrada).

Command é teclado-first: o valor não é discoverability (ninguém vai "explorar" os comandos), é velocidade de targeting para usuários recorrentes. Não use para usuários novos.

Preview
Command inline com grupos e shortcuts.

Anatomy

<Command>                       ← root (cmdk Command)
  ├─ <CommandInput />            ← busca com SearchIcon embutido
  └─ <CommandList>
       ├─ <CommandEmpty />        ← mostrado quando filter retorna 0
       ├─ <CommandGroup heading>
       │    ├─ <CommandItem />    ← um por entrada
       │    └─ <CommandShortcut /> (opcional, dentro do Item)
       └─ <CommandSeparator />
</Command>

<CommandDialog>                  ← envelope que injeta Dialog ao redor
  └─ ... (mesmo conteúdo)
</CommandDialog>

Usage

"use client";

import * as React from "react";
import {
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut
} from "@kalvner/kds/overlays/command";

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

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

  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandInput placeholder="Type a command or search... ⌘K" />
      <CommandList>
        <CommandEmpty>No results.</CommandEmpty>
        <CommandGroup heading="Navigation">
          <CommandItem onSelect={() => navigate("/inbox")}>
            Inbox
            <CommandShortcut>G I</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </CommandDialog>
  );
}

Props

Command (Root) e CommandDialog

PropTipoDefaultDescrição
value / onValueChangestring / (v) => voidItem selecionado (para controle externo).
filter(value, search) => numberCustom filter (default: cmdk substring).
loopbooleanfalse↑/↓ dão wrap.

CommandDialog herda todos os props de Dialog, além de title/description (sr-only por padrão).

CommandInput

PropTipoDefaultDescrição
placeholderstringInclua sempre uma dica de teclado.
value / onValueChangestring / (v) => voidControlado opcional.

CommandItem

PropTipoDefaultDescrição
valuestring(texto do filho)Override do que o filtro busca.
onSelect(value: string) => voidDisparado ao Enter ou clique.
disabledbooleanfalseBloqueia este item.

Subcomponents

  • CommandInput — input com SearchIcon embutido.
  • CommandList — wrapper rolável (max-h-[300px]).
  • CommandEmpty — render condicional automático em zero resultados.
  • CommandGroup — grupo com heading sticky.
  • CommandItem — entrada selecionável; aceita ícone + texto + shortcut.
  • CommandShortcut<span> posicionado à direita com tracking.
  • CommandSeparator — divisor de 1px.
  • CommandDialog — envolve tudo num Dialog com header sr-only.

Variants

Inline (sem dialog)
Usado dentro de Popover para Combobox.

States

EstadoComportamento
Item data-selected=trueBackground accent, foreground accent-foreground.
Item data-disabled=truePointer-events none, opacity 50%.
Empty (filter=0)CommandEmpty aparece em vez dos grupos.

Composition

Combobox pattern

Command inline dentro de Popover é o padrão Combobox canônico (autocomplete acessível). O trigger é um botão; ao abrir, foco vai para o CommandInput.

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">
      {value ? value : "Selecione um framework"}
    </Button>
  </PopoverTrigger>
  <PopoverContent className="w-[280px] p-0">
    <Command>
      <CommandInput placeholder="Buscar..." />
      <CommandList>
        <CommandEmpty>Nada encontrado.</CommandEmpty>
        <CommandGroup>
          {options.map((opt) => (
            <CommandItem key={opt} onSelect={(v) => setValue(v)}>
              {opt}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  </PopoverContent>
</Popover>

⌘K palette

Veja Usage acima. A regra: um único atalho global por app (não pulverize por feature).

When to use

  • App com 5+ páginas onde usuários recorrentes querem pular sem clicar.
  • Combobox com lista grande (>10 itens) onde busca é necessária.
  • Switcher de workspace / project / org.
  • Editor com paleta de comandos (Notion, Linear).

When not to use

  • App pequeno (menos de 5 destinos) — atalho não amortiza descoberta.
  • Lista com 5 itens ou menos sem busca útil — use Select ou DropdownMenu.
  • Usuários novos só — eles não vão descobrir ⌘K.
  • Mobile-only — ⌘K não tem equivalente touch fluido.

Best practices

  • Sempre inclua dica de teclado no placeholder ("Type a command... ⌘K").
  • Agrupe semanticamente — Navigation, Actions, Recent. Sem grupo, vira lista plana e perde o ponto.
  • Mostre Recent no topo (estado serializado por usuário) — top-of-mind vence ordem alfabética.
  • Não exceda ~30 itens visíveis sem filtro — depois disso, o usuário nunca scrolla, só busca.
  • Em mobile, prefira Drawer com lista — ⌘K é desktop-pattern.

Accessibility

ConcernComportamento
Comboboxcmdk aplica role="combobox" no input com aria-controls e aria-expanded.
ListboxA lista é role="listbox", items são role="option".
Keyboard/ navegam, Enter seleciona, Esc fecha (no Dialog).
FocusCommandDialog move foco pro input ao abrir; restaura ao fechar.
TitleCommandDialog requer title (sr-only ok).
Live filterCommandEmpty é anunciado ao mudar de "tem resultados" para "não tem".
  • Dialog — base do CommandDialog.
  • Popover — wrapper do Combobox.
  • Select — escolha discreta sem busca.
  • Sheet — equivalente mobile para "paleta lateral".

On this page