Substituto do <select> nativo. Botão que herda a tipografia do campo, seta que gira, lista flutuante estilizável, navegação completa por teclado e ARIA. Escreve num <input type="hidden">, então o formulário envia como qualquer outro campo.
Existe por um motivo só: nenhum navegador deixa estilizar a lista de opções de um <select> nativo. Num formulário escuro e com tipografia de marca, o dropdown do sistema destoa de tudo.
Quando usar
- Formulário com identidade visual forte onde o
<select>do sistema quebraria o conjunto. - Listas de 3 a 8 opções curtas: faixas de valor, estados, tipo de projeto, orçamento.
Quando não usar
- Listas longas (país, cidade, CNAE). Sem busca e sem digitação para filtrar, 200 opções viram tortura. Use o nativo, ou um combobox com busca.
- Quando a acessibilidade precisa ser à prova de tudo. O nativo é sempre mais robusto que qualquer reimplementação — este cobre o caminho comum bem, não todos os casos.
- Em formulário sem identidade visual própria. Aí o nativo é melhor: é conhecido, rápido e funciona offline do CSS.
Padrão editorial
| Slot | Regra |
|---|---|
| Label | 2 a 4 palavras, caixa alta por CSS: "Capital para investir". |
| Placeholder | "Selecione" mais o substantivo: "Selecione uma faixa". Nunca uma opção real como placeholder. |
| Opções | 3 a 8. Texto curto o bastante para caber em uma linha. Ordem crescente, ou alfabética — nunca "a mais escolhida primeiro". |
Faixas de valor são melhores que campo aberto em formulário de qualificação: o visitante não precisa revelar o número exato, e você segmenta igual.
Como usar
<link rel="stylesheet" href="componentes/conversao/select-custom/estilo.css">
<script src="componentes/conversao/select-custom/script.js"></script>
O script varre [data-csel], então vários selects na mesma página funcionam — e abrir um fecha os outros.
Ganchos
| Gancho | Papel |
|---|---|
[data-csel] |
o campo |
[data-csel-required] |
marca como obrigatório, lido pela validação do formulário |
.csel.is-open |
lista aberta |
.csel.has-error |
dispara o tremor de validação |
.csel-val.is-set |
valor escolhido (muda a cor do placeholder) |
input[type=hidden] |
onde o valor é escrito; o name dele é o que o backend recebe |
Teclado
| Tecla | Fechado | Aberto |
|---|---|---|
| ↓ / Enter / Espaço | abre | desce / escolhe |
| ↑ | — | sobe |
| Home / End | — | primeira / última |
| Esc | — | fecha e devolve o foco ao botão |
Gotchas
- O
<input type="hidden">é o componente. Sem ele nada é enviado. Onamedele é o que chega ao backend — odata-valueda opção é só o texto. - A lista precisa de
hiddenno HTML. Sem isso ela aparece por um instante no carregamento e fica tabulável fechada. - O botão é
type="button". Dentro de um<form>, sem isso ele envia o formulário ao ser clicado. e.stopPropagation()no clique do botão — senão o listener de "clicou fora" nodocumentfecha a lista no mesmo clique que a abriu.aria-labelledbyno botão e na lista aponta para oiddo label. Cada select na página precisa deidpróprio; repetir quebra o anúncio do leitor de tela.- O tremor de erro é o único retorno visual. O campo não tem
:invalidnativo, então quem valida (o formulário) precisa aplicar.has-error. Com movimento reduzido ele vira um contorno, não some. .csel-opttemtabindex="-1". As opções não entram na ordem de Tab: quem navega é a seta, dentro do widget. É como o<select>nativo se comporta.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| Liberta Wealth | index.html, campo "Capital para investir" do #diagnostico |
2026-08 | Origem. Prefixo cselect-, estado .open, classe de erro .shake, cores #f8f6f2 / #66645e / #1a1a1a / #d4d1ca cruas, sem navegação por teclado dentro da lista (só Esc), sem tabindex nas opções, sem aria-labelledby na lista. |
Normalizações aplicadas na extração: prefixo cselect- → csel-; .open → .is-open, .shake → .has-error; cores cruas → --fg / --fg-muted / --dark-2 / --rule-2; adicionada navegação completa por teclado (setas, Home, End, Enter, Espaço) e o estado .is-active que ela precisa; adicionado tabindex="-1" nas opções e aria-labelledby na lista; scrollIntoView({block:'nearest'}) para a lista acompanhar a seta; adicionado fallback de prefers-reduced-motion no erro; wrapper de campo separado do widget, para o select poder viver dentro de qualquer formulário.