Combobox

Formulario

Seleção única com busca, composta a partir de popover e command, pronta para uso com uma lista de opções.

Carregando

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">
PropTipoDescrição
optionsComboboxOption[]
value?stringValor selecionado (controlado). Combine sempre com `onValueChange`.
onValueChange?(value: string) => void
placeholder?stringTexto do trigger quando nada esta selecionado, e rotulo acessivel do campo de busca.
emptyMessage?stringMensagem exibida quando a busca nao encontra nenhuma opcao.
contentClassName?stringClasse 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? }. value e o identificador estavel (o que onValueChange recebe); label e 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