Estilizando o dropdown de um select com base-select
Estilize menus select nativos com appearance: base-select. Explore o picker, as opções, os ícones, a acessibilidade, o suporte dos navegadores e as limitações.
Definir appearance: base-select em um <select> e no seu pseudoelemento ::picker(select) permite estilizar o controle inteiro com CSS: o botão, o dropdown, a seta, o checkmark e cada uma das opções.
Muitas equipes mantêm um componente de dropdown personalizado por um único motivo: o <select> nativo não podia ser estilizado de acordo com o design. O tratamento de teclado, o gerenciamento de foco e a configuração de ARIA vieram como efeito colateral. O appearance: base-select elimina esse motivo.
Este guia percorre todas as possibilidades de estilização, desde a ativação até as opções ricas, e aborda o que você perde. Para técnicas clássicas com appearance: none, o artigo estilizando elementos select com CSS moderno oferece um panorama mais amplo, mas trata o base-select apenas brevemente. Abordagens mais antigas são discutidas em estilizando dropdowns de select sem JavaScript.
Principais conclusões
appearance: base-selectprecisa ser definido tanto emselectquanto em::picker(select). Defini-lo apenas no picker não tem efeito algum.- O picker é um popover na top layer, ancorado ao botão do select por meio de uma referência de âncora implícita, então você não precisa de
z-index, portals nemanchor-name. - O
<selectedcontent>fica dentro de um<button>que é o primeiro filho do<select>e exibe um clone da opção selecionada no controle fechado. - Toda opção deve conter texto, porque opções apenas com ícones não dão aos leitores de tela nada para anunciar e aparecem vazias em navegadores sem suporte.
- Com o base-select, você abre mão do picker nativo do sistema operacional em dispositivos móveis, da renderização fora da área do navegador e do dimensionamento automático de acordo com a opção mais longa.
O componente que você pode excluir
Um componente de select personalizado geralmente existe apenas para corrigir a aparência do elemento <select> nativo e, para isso, acaba assumindo muitos comportamentos. O post de lançamento da equipe do Chrome informa que, a partir do Chrome 135, é possível estilizar o elemento <select> com CSS, mantendo a acessibilidade e o comportamento padrão de um controle nativo. O WebKit seguiu o mesmo caminho no Safari 27.0, em que o <select> de verdade agora pode ser reestilizado de cima a baixo. Quando um navegador oferece suporte, o principal motivo para manter um listbox baseado em divs desaparece, assim como o comportamento que você precisava reconstruir para sustentá-lo.
Como ativar o appearance: base-select?
Para ativá-lo, aplique appearance: base-select tanto ao <select> quanto ao seu picker. O guia de select personalizável da MDN explica que o select pode ser ativado sozinho, mas o picker só funciona quando o select também está ativado. As orientações do WebKit sobre select personalizável recomendam envolver essas melhorias em @supports (appearance: base-select). Mantenha todas as regras seguintes dentro do mesmo bloco, para que navegadores sem suporte recebam o select comum.
@supports (appearance: base-select) {
select,
::picker(select) {
appearance: base-select;
}
/* all following rules go here */
}
| Parte | Seletor | Observações |
|---|---|---|
| Controle fechado | select | O botão do select |
| Seta | ::picker-icon | Não faz parte da árvore de acessibilidade |
| Estado aberto | :open | Corresponde enquanto o picker está visível |
| Dropdown | ::picker(select) | Tudo, exceto o botão que é o primeiro filho |
| Cada opção | option | display: flex por padrão |
| Opção selecionada | option:checked | |
| Marca de seleção | ::checkmark | Não faz parte da árvore de acessibilidade |
| Conteúdo no estado fechado | selectedcontent | Clone da opção selecionada |
Estilize o controle fechado
O controle fechado é o elemento select. A seta é o ::picker-icon, e :open corresponde enquanto o picker está visível, então você pode girar a seta quando o select abrir.
select {
border: 2px solid #d4d4d8;
background: #f4f4f5;
padding: 10px;
transition: 0.4s;
}
select:hover,
select:focus {
background: #e4e4e7;
}
select::picker-icon {
color: #71717a;
transition: 0.4s rotate;
}
select:open::picker-icon {
rotate: 180deg;
}
O transition em rotate faz a seta girar suavemente sempre que o picker abre e fecha.
Estilize a lista aberta
A lista dropdown aberta de um select personalizável é o ::picker(select). O navegador agrupa nele tudo o que está dentro do <select>, exceto o botão que vem primeiro. As opções são flex containers por padrão. O :checked seleciona a opção marcada, e o ::checkmark seleciona a marca ao lado dela.
::picker(select) {
border: none;
border-radius: 8px;
}
option {
display: flex;
gap: 16px;
padding: 10px;
}
option:hover,
option:focus {
background: #ede9fe;
}
option:checked {
font-weight: bold;
}
option::checkmark {
order: 1;
margin-left: auto;
content: "✔";
}
Definir order: 1 junto com margin-left: auto move o checkmark do início da linha para o final.
Posicionamento e animação
Em um select personalizável, o dropdown é um popover que abre na top layer. O navegador o vincula ao botão do select automaticamente usando CSS anchor positioning, então você não precisa de z-index, de um portal nem de configurar anchor-name. O botão e o picker têm a mesma relação invoker/popover que o guia de invoker commands descreve para botões e popovers. Os estilos padrão também reposicionam o picker para outro lado caso ele ultrapasse os limites da tela, e você pode definir sua própria posição com anchor():
::picker(select) {
top: calc(anchor(bottom) + 1px);
left: anchor(10%);
}
Para aplicar um efeito de fade ao abrir e fechar o picker, crie uma transição entre seus estados de popover:
::picker(select) {
opacity: 0;
transition: all 0.4s allow-discrete;
}
:open::picker(select) {
opacity: 1;
}
@starting-style {
:open::picker(select) {
opacity: 0;
}
}
O allow-discrete permite que as propriedades discretas display e overlay participem da transição. Sem ele, o picker sairia da top layer antes que o fade-out pudesse ser visto. O @starting-style define a opacidade a partir da qual o fade-in começa.
Opções ricas e selectedcontent
Coloque o <selectedcontent> dentro de um <button> que seja o primeiro filho do <select>. Ele exibe um clone do conteúdo da opção selecionada no controle fechado, e você pode estilizá-lo separadamente das opções do dropdown. As opções agora podem conter marcação, como ícones e imagens.
<label for="status">Status</label>
<select id="status" name="status">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="draft">
<span class="icon" aria-hidden="true">📝</span>
<span class="option-label">Draft</span>
</option>
<option value="review">
<img class="icon" src="review.svg" alt="">
<span class="option-label">In review</span>
</option>
<option value="published">
<span class="icon" aria-hidden="true">✅</span>
<span class="option-label">Published</span>
</option>
</select>
A MDN observa que os ícones podem deixar o botão fechado mais alto e deslocar a seta. Você pode ocultá-los apenas no estado fechado:
selectedcontent .icon {
display: none;
}
As opções no picker aberto mantêm seus ícones. O botão do select é inerte, então links ou botões colocados dentro dele não podem receber foco nem ser clicados individualmente, e o conjunto todo se comporta como um único botão. A MDN também ressalta que conteúdo extra no botão pode alterar o que os leitores de tela informam como valor do select, então limite o conteúdo do botão ao <selectedcontent>.
O que o navegador faz por você?
Com o base-select, o navegador cuida de tudo o que um dropdown personalizado precisa reimplementar: interação por teclado, foco, empilhamento na top layer, envio do valor no formulário e semântica para leitores de tela. O WebKit afirma que o suporte a teclado e a acessibilidade já vêm embutidos, porque continua sendo o controle nativo do próprio navegador. Ao reproduzir sessões de usuários em um dropdown baseado em divs, é comum encontrar bugs de teclado e de foco, justamente os bugs que um select nativo evita por design. Para ter uma ideia de quanto código a abordagem manual exige, veja este guia sobre como construir um dropdown select acessível do zero.
Duas regras ainda se aplicam:
- Mantenha texto em todas as opções. A principal regra do WebKit é que cada opção precisa ter texto real ou um atributo de texto acessível. Texto oculto visualmente continua sendo lido pelos leitores de tela. Imagens decorativas recebem
alt=""e deixam que o rótulo de texto transmita o significado. - Trate ícones personalizados como decoração. Os leitores de tela ignoram tudo o que você adiciona com
contentem::checkmarke::picker-icon, então uma seta ou um checkmark personalizado nunca é lido em voz alta.
O código JavaScript existente que lê o valor do select continua funcionando. Se uma opção não tiver o atributo value, o valor do select será o textContent da opção selecionada, sem os espaços em branco nas extremidades. Portanto, sempre defina value quando as opções contiverem marcação rica.
Suporte, fallback e trade-offs
O appearance: base-select está disponível no Chrome 135 e posteriores, em outros navegadores baseados em Chromium, como o Edge, e no Safari 27.0. O Firefox começou a implementá-lo atrás de uma preferência: o valor appearance: base-select chegou ao Firefox 149 atrás de uma flag, mas não está habilitado por padrão. A MDN ainda o classifica como de disponibilidade limitada, então consulte as tabelas de compatibilidade nas páginas de referência da MDN. O Safari traz seus próprios estilos padrão para o base-select, então controles sem estilização não terão a mesma aparência em todos os navegadores.
Navegadores sem suporte ao base-select ignoram a marcação de <button> e <selectedcontent>. Eles renderizam um select comum que funciona normalmente, com cada opção reduzida ao seu texto. Esse fallback só faz sentido se o texto estiver presente.
O post do Chrome também lista três comportamentos que você perde:
- A lista aberta fica restrita à janela do navegador.
- Em celulares e tablets, você recebe o picker do navegador em vez do picker nativo do sistema operacional.
- A largura do select deixa de acompanhar a opção mais longa, então defina você mesmo um
widthemselect, ::picker(select).
O base-select também altera a forma como o parser HTML interpreta o conteúdo de um <select>. A MDN alerta que isso pode entrar em conflito com frameworks JavaScript: alguns bloqueiam os recursos e, em outros, a renderização no lado do servidor pode quebrar a hidratação. Teste o recurso no seu framework antes de remover o componente existente.
Conclusão
Um select nativo com appearance: base-select oferece controle sobre a estilização, enquanto o navegador continua cuidando de teclado, foco, empilhamento e acessibilidade. Para experimentar, escolha um dropdown personalizado simples, reconstrua-o dentro de um bloco @supports com texto em todas as opções e teste-o em um navegador sem suporte ao base-select. Se ele continuar funcionando como um select comum nesse navegador e os trade-offs mencionados acima forem aceitáveis, você pode remover o componente personalizado.
Perguntas frequentes
Qual é a diferença entre appearance: none e appearance: base-select em um select?
O appearance: none apenas remove a estilização nativa do botão fechado. A lista dropdown e suas opções continuam usando o picker do navegador ou do sistema operacional, e a maior parte do CSS não as alcança. Já o appearance: base-select, definido tanto em select quanto em ::picker(select), também permite estilizar o picker, as opções, a seta e o checkmark, além de permitir HTML rico dentro das opções. Navegadores sem suporte o ignoram e mantêm o controle clássico.
Como detectar o suporte ao base-select em JavaScript?
Chame CSS.supports('appearance', 'base-select'). Ele retorna true apenas em navegadores que reconhecem esse valor. A verificação é a mesma da condição CSS @supports (appearance: base-select), então você pode usá-la para decidir se carrega um componente de dropdown de fallback ou se adiciona uma classe que ativa os estilos personalizados. Use detecção de recursos em vez de user agent sniffing, porque cada engine adicionou o suporte em um momento diferente.
O base-select funciona com select multiple ou com o atributo size?
Sim, no modo listbox. O atributo multiple, ou um valor de size maior que 1, exibe as opções como uma caixa na página em vez de um botão que abre um popup. A partir do Chrome 145, o base-select também funciona nesse modo, tanto para seleção única quanto múltipla. Ele ainda não abrange o popup de seleção múltipla obtido com multiple e size=1. Um listbox personalizável não tem picker, então ::picker(select), ::picker-icon e selectedcontent não se aplicam. Consulte as tabelas de compatibilidade da MDN para verificar o suporte em outras engines.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.
Star on GitHub12k