Combobox
Seleção única com busca, composta a partir de popover e command, pronta para uso com uma lista de opções.
Instalação
Recebe correções por update de versão. O caminho recomendado.
npm i @trdr/ui
import { Combobox } from "@trdr/ui/combobox"Uso
import { Combobox, ComboboxOption } from "@trdr/ui/combobox"
export function Exemplo() {
return <Combobox />
}Props
ComboboxPropsestende Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "value" | "onChange">| Prop | Tipo | Descrição |
|---|---|---|
| options | ComboboxOption[] | |
| value? | string | Valor selecionado (controlado). Combine sempre com `onValueChange`. |
| onValueChange? | (value: string) => void | |
| placeholder? | string | Texto do trigger quando nada esta selecionado, e rotulo acessivel do campo de busca. |
| emptyMessage? | string | Mensagem exibida quando a busca nao encontra nenhuma opcao. |
| contentClassName? | string | Classe do painel flutuante (`PopoverContent` por baixo). |
Quando usar
Use o Combobox no lugar do Select quando a lista de opcoes for longa o suficiente para que
digitar seja mais rapido que rolar (uma lista de centenas de ativos, por exemplo). Ele resolve o
caso comum de campo de formulario com busca: um trigger que parece um Select, um valor
controlado (value/onValueChange) e uma lista filtravel por dentro. Diferente do Command, que
e pensado como paleta global de comandos, o Combobox e um campo dentro do fluxo normal de um
formulario.
Quando nao usar
Para poucas opcoes (ate uns 6 a 8) sem necessidade de busca, o Select e mais simples de abrir e
operar. Para selecao multipla, nenhum dos dois serve: isso pede uma composicao propria com
Checkbox dentro da lista, fora do escopo deste componente. Para busca de comandos e acoes fora
de um formulario (nao um valor de campo), use o Command/CommandDialog diretamente.
Anatomia
Combobox e a versao pronta de usar: um unico componente, nao uma familia de partes. Por baixo
ele monta
Popover(PopoverTrigger+PopoverContent): a superficie flutuante e o clique que abre e fecha.Command(CommandInput,CommandList,CommandEmpty,CommandGroup,CommandItem): a busca e a lista filtravel dentro do painel.
Props
options: array de{ value, label, disabled? }.valuee o identificador estavel (o queonValueChangerecebe);labele o texto exibido e o que a busca filtra.value/onValueChange: estado controlado da selecao.placeholder: texto do trigger quando nada esta selecionado, e tambem o rotulo acessivel do campo de busca interno.emptyMessage: mensagem exibida quando a busca nao encontra nenhuma opcao.contentClassName: classe do painel flutuante, para os casos raros em que a largura ou o max-height padrao nao servem.
Por que nao expor Popover/Command crus aqui
A composicao crua (Popover + Command montados a mao, item por item) e o jeito certo quando
voce precisa de algo que options/value nao cobrem: um layout de item com avatar e subtitulo,
carregamento assincrono de opcoes, seguindo grupos, ou uma acao extra no rodape da lista. Para o
caso do dia a dia (uma lista estatica de opcoes com rotulo e valor) essa composicao e verbosa
demais para repetir em cada tela, por isso o Combobox existe como componente pronto. Quando
precisar da versao crua, monte assim:
import { useState } from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { Button } from "@trdr/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@trdr/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "@trdr/ui/popover"
function ComboboxCru() {
const [open, setOpen] = useState(false)
const [value, setValue] = useState<string>()
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" role="combobox" aria-expanded={open}>
{value ?? "Selecionar..."}
<ChevronsUpDown className="size-4" aria-hidden="true" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" aria-label="Selecionar ativo" className="p-0">
<Command label="Selecionar ativo">
<CommandInput placeholder="Buscar..." />
<CommandList>
<CommandEmpty>Nenhum resultado.</CommandEmpty>
<CommandGroup>
{/* seu layout de item, seu carregamento assincrono, seus grupos */}
<CommandItem
value="btcusd"
onSelect={(v) => {
setValue(v)
setOpen(false)
}}
>
<Check className={value === "btcusd" ? "opacity-100" : "opacity-0"} />
BTC/USD
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}Acessibilidade
O trigger carrega role="combobox" e aria-expanded, o mesmo padrao do SelectTrigger. O texto
visivel do trigger (o valor selecionado ou o placeholder) e o proprio nome acessivel do botao;
nao sobreponha com aria-label, ou quem usa leitor de tela deixa de ouvir qual ativo esta
selecionado. O painel (PopoverContent) recebe aria-label a partir do placeholder
automaticamente. O campo de busca interno usa a prop label do Command para o mesmo fim (ver
MDX do Command, secao Acessibilidade).
Teclado: abrir com Enter/Espaco/seta no trigger (comportamento do Popover), digitar filtra,
ArrowUp/ArrowDown navegam pulando itens disabled, Enter confirma e fecha, Escape fecha
sem alterar o valor.
Slots e tokens
Todo elemento renderizado carrega um data-slot para estilizar partes internas sem depender de classe da biblioteca. Os tokens listados são extraídos do código fonte.
[data-slot="combobox-content"][data-slot="combobox-trigger"][data-slot="command"]2 tokens usados
text-content-placeholdertext-content-tertiary