A propriedade CSS appearance: base-select permite estilizar um elemento <select> e o seletor dele (via ::picker(select)) como elementos HTML comuns.
Combinado com técnicas modernas de layout CSS, como Grid ou Flexbox, é possível se afastar da tradicional lista vertical empilhada e criar uma experiência visual rica sem JS personalizado.
Por exemplo, use display: grid para estilizar as opções como uma grade 2D de cards.
Isso é ideal para seletores de cores, seletores de emojis ou variantes de produtos em que um menu visual avançado é mais eficaz do que uma lista.
Como implementar
1. Ativar estilo básico
Aplique appearance: base-select ao elemento <select> e ao pseudoelemento do seletor interno select::picker(select):
select.grid-picker {
&, &::picker(select) {
appearance: base-select;
}
}
2. Estilizar o contêiner do seletor
Segmente select:open::picker(select) e aplique display: grid (ou display: flex).
Defina colunas e lacunas como faria para qualquer contêiner.
select.grid-picker:open::picker(select) {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
IMPORTANTE: o :open é necessário. Se você estilizar select::picker(select), o seletor vai ficar visível mesmo quando o elemento select estiver fechado.
3. Estilizar as opções
Segmente os elementos <option> para estilizá-los como itens de grade ou cards.
É possível adicionar imagens, SVGs ou layouts complexos dentro deles.
/* Style options as grid cards */
.grid-picker option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* padding, border-radius, background, etc. */
}
4. Estilizar a opção selecionada
Para definir o estilo da opção selecionada, use option:checked. Exemplo:
.grid-picker option:checked {
position: relative;
border: 2px solid var(--primary-color);
background-color: oklab(from var(--primary-color) l a b / 8%);
}
Como em qualquer controle de interface, não use apenas cores para comunicar o estado. Use vários indicadores visuais (por exemplo, cores, espessura da borda, marca de seleção) para comunicar qual opção está selecionada.
5. (Opcional) Estilizar a marca de seleção da opção selecionada
Por padrão, o navegador renderiza uma marca de seleção como o primeiro filho de cada <option>.
Ele se comporta como um ::before com content: "✓" / "", renderizado antes de um ::before fornecido pelo autor, se houver.
Se a opção não estiver selecionada, a marca de seleção será ocultada por visibility: hidden.
É possível estilizá-lo usando o pseudoelemento ::checkmark ou ocultá-lo totalmente se você quiser transmitir a seleção apenas por meio de estilo ou usar um indicador personalizado.
Por exemplo, para colocar o ícone no canto superior direito da opção, faça o seguinte:
select.grid-picker {
> option::checkmark {
/* Hide the checkmark for unselected options */
display: none;
}
> option:checked {
position: relative;
&::checkmark {
position: absolute;
inset-block-start: -.4em;
inset-inline-end: -.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-inline-size: 1.6em;
aspect-ratio: 1;
padding-right: 0.12em;
padding-left: 0.05em;
border-radius: 50%;
background: var(--primary-color);
color: white;
font-size: 70%;
font-weight: bold;
}
}
}
Dica: ao pintar um contêiner (por exemplo, um círculo) ao redor da marca de seleção, empurre-o um pouco para cima (como fizemos aqui com padding-bottom e padding-left) para uma centralização óptica que considere a forma dele.
NÃO use padding-inline-start ou padding-block-end para isso, porque eles mudam de acordo com o modo de escrita, mas o formato da marca de seleção não.
6. (Opcional) Personalizar a exibição da opção selecionada
Por padrão, o <select> mostra uma versão somente de texto da opção selecionada.
Para mostrar conteúdo avançado para o valor selecionado, use <selectedcontent> dentro de um <button> e/ou para mostrar a opção selecionada de forma diferente (por exemplo, ocultar determinados detalhes).
NÃO use <selectedcontent> sem o <button> ao redor, porque o <selectedcontent> vai se tornar parte do seletor, como qualquer outro elemento de conteúdo.
Práticas recomendadas
- Teste o comportamento do layout. Definir
appearance: base-selectremove o comportamento padrão do navegador de dimensionar a seleção com base na largura da opção mais longa. Talvez seja necessário definir uma largura fixa ou usar restrições de flex/grid para evitar mudanças de layout. - Verifique se o
<select>tem um atributonamee um<label>associado. Isso garante que, mesmo com uma interface personalizada, o componente permaneça acessível para leitores de tela e funcione corretamente com envios de formulários padrão. - Use atributos
valuepadrão nas opções para garantir que o envio do formulário funcione exatamente como antes.
Acessibilidade
Um <select> nativo só lida com a navegação linear de setas para cima/baixo. Portanto, as setas para esquerda/direita não fazem nada no seletor aberto, e as setas para cima/baixo ainda navegam pelos itens sequencialmente, independentemente de linhas e colunas.
Para implementar a navegação espacial que segue a grade, intercepte keydown e direcione o foco para a opção mais próxima na direção pressionada (por exemplo, usando getBoundingClientRect()) quando as teclas de seta forem usadas.
Para determinar o item anterior/seguinte adequado, use métodos DOM que operam na geometria real (por exemplo, getBoundingClientRect()) em vez de analisar strings CSS como grid-template-columns ou grid-column, que podem ser frágeis e propensas a erros em grades complexas, subgrades, RTL e outros cenários de layout.
Dito isso, não complique demais sem necessidade. Use as invariantes que você sabe que são verdadeiras sobre o layout para simplificar essa lógica.
Observação: isso apenas aborda a navegação pelo teclado. O <select> e o ::picker() ainda têm os papéis padrão (menu ou listbox), que não podem ser substituídos nem conter um elemento com role="grid".
Aninhar um wrapper com role="grid" dentro do seletor funciona na prática, mas não é válido e pode surpreender os usuários de TA.
Em alguns casos, usar <optgroup> para agrupar opções pode produzir uma boa UX de leitor de tela sem role="grid".
Caso contrário, pode ser necessário mudar para uma solução personalizada usando a API Popover, o posicionamento de âncora e role="grid" para alcançar a UX desejada para usuários de leitores de tela.
Estratégia de fallback
Alternativas e suporte do navegador para <select> personalizável
Para navegadores que ainda não são compatíveis com appearance: base-select, o elemento <select> é degradado de forma otimizada para um menu suspenso padrão do sistema operacional.
- Conteúdo não textual ignorado: navegadores mais antigos removem tags HTML (como
<svg>ou<div>) dentro de tags<option>e renderizam apenas os nós de texto. Verifique se o conteúdo de texto do<option>é legível e significativo por si só. - Processamento da estrutura HTML: os analisadores padrão podem ignorar as tags
<button>e<selectedcontent>dentro de<select>ou tratá-las como inválidas. Não é necessário usar polyfills pesados de JavaScript para o aprimoramento progressivo se você considerar o texto padrão como um substituto legível.
document.addEventListener("DOMContentLoaded", () => {
// Check if browser supports base-select value
if (!CSS.supports("appearance", "base-select")) {
// Custom select overrides are not supported natively.
}
});