Combobox

Pronto

Select com busca embutida. Ideal para listas grandes (50+ opções) ou quando velocidade de digitação supera scroll.

Usar quando

Buscar tribunal, comarca, advogado, contato em listas grandes. Quando usuário sabe o que procura.

Não usar quando

Lista pequena (<10, use Select). Lista exibida sempre (use lista filtrada inline).

Variantes

TJSP — São PauloTRT2 — Trabalho SP

Estados

Closed
Open + buscando
  • TJSP — São Paulo
  • TJRJ — Rio de Janeiro
  • TJMG — Minas Gerais
  • TJRS — Rio Grande do Sul
  • TJPR — Paraná
  • TJSC — Santa Catarina
  • TJBA — Bahia
  • TJPE — Pernambuco
  • TJCE — Ceará
  • TJGO — Goiás
  • TRF1 — Federal 1ª Região
  • TRF2 — Federal 2ª Região
  • TRF3 — Federal 3ª Região
  • TRT2 — Trabalho SP
  • TRT15 — Trabalho Campinas
  • STF — Supremo Tribunal Federal
  • STJ — Superior Tribunal de Justiça
  • TST — Tribunal Superior do Trabalho
Resultado vazio
  • Sem resultados para "ação penal pública incondicionada".
Loading async

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Open dropdownfocus no inputListbox abre com fade200ms cubic-bezier(0.32,0.72,0,1)
Filter liveonChange do inputFiltra opções incrementalmenteinstant (sync) ou 300ms debounce (async)
Highlight termobusca tem matchesSubstring buscada fica destacada com brass-tintinstant
Chip add (multi)select itemChip aparece com scale 0.8→1200ms
Chip removeclick X no chipChip diminui horizontalmente + opacity 1→0150ms
Loading shimmerasync fetch in flightSkeletons brilhando no listbox1500ms loop
Empty resultbusca sem matchesMostra "Sem resultados para X" + sugestão limparinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco no input
DigitarFiltra opções
↓ / ↑Navega resultados
EnterSeleciona em foco
EscFecha sem alterar
Backspace (multi, input vazio)Remove último chip
ARIA esperado
  • role="combobox" aria-haspopup="listbox" aria-expanded
  • aria-autocomplete="list"
  • aria-controls="listbox-id"
  • aria-activedescendant="opt-id"
  • <ul role="listbox"><li role="option" aria-selected />
Notas
  • Multi-select: use chips visíveis no input + Backspace remove último.
  • Async: debounce 300ms entre keystroke e fetch.
  • Loading: mostre skeletons no listbox, NÃO bloqueie input.
  • Empty result com sugestão (típico: "Limpar busca" ou "Criar X com este nome").

Código

'use client';
import { useState, useMemo } from 'react';
import { Check } from 'lucide-react';
import { T, TYPE, SP, RADIUS, transition } from '@/lib/tokens';

interface ComboboxProps<T extends { value: string; label: string }> {
  options: T[];
  value: string | null;
  onChange: (v: string | null) => void;
  placeholder?: string;
}

export function Combobox<T extends { value: string; label: string }>({
  options, value, onChange, placeholder
}: ComboboxProps<T>) {
  const [query, setQuery] = useState('');
  const [open, setOpen] = useState(false);
  const filtered = useMemo(
    () => options.filter(o => o.label.toLowerCase().includes(query.toLowerCase())),
    [options, query]
  );
  const selected = options.find(o => o.value === value);

  return (
    <div style={{ position: 'relative' }}>
      <input
        type="text"
        role="combobox"
        aria-haspopup="listbox"
        aria-expanded={open}
        value={open ? query : selected?.label ?? ''}
        onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
        onFocus={() => setOpen(true)}
        onBlur={() => setTimeout(() => setOpen(false), 200)}
        placeholder={placeholder}
        // ... styles
      />
      {open && (
        <ul role="listbox">
          {filtered.map(o => (
            <li key={o.value} role="option" aria-selected={o.value === value}
              onMouseDown={() => { onChange(o.value); setOpen(false); setQuery(''); }}>
              {o.label}
              {o.value === value && <Check size={14} />}
            </li>
          ))}
          {filtered.length === 0 && <li>Sem resultados para "{query}"</li>}
        </ul>
      )}
    </div>
  );
}

Regras

Faça

  • Use para listas com 50+ opções ou onde busca acelera.
  • Filter live (sync) se lista <500; async com debounce 300ms se >500.
  • Multi: use chips com X para remover.
  • Highlight do termo buscado no resultado (brass-tint).
  • Empty result com mensagem amigável + ação ("Criar X com esse nome").
  • Loading: skeleton no listbox, input continua editável.

Não faça

  • Não use para 5-50 opções fixas (use Select).
  • Não bloqueie digitação durante async (deixe usuário digitar adiante).
  • Não force scroll infinito sem feedback visual.
  • Não use modal pra esconder o combobox — ele já é compacto.
  • Não esconda chips quando focou no input (multi).

Tokens usados

TokenValorPapel
T.surface#FFFFFFbackground
T.surfaceHover#F7F5EChover de item
T.brassTintrgba(164,124,43,0.08)background do chip + highlight
T.markrgba(164,124,43,0.22)highlight de termo buscado
RADIUS.md12borderRadius do input
RADIUS.pill980chip