Layouts personalizados do seletor de seleção

Recursos da Web: <select> personalizável

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-select remove 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 atributo name e 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 value padrã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.
  }
});