Seletor de rich media (seleção personalizável)

Recursos da Web: <select> personalizável

O elemento nativo <select> era historicamente difícil de estilizar e só podia conter opções de texto simples. A propriedade appearance: base-select oferece uma maneira declarativa, somente CSS, de ativar um estado personalizável para o elemento <select>. Isso permite que os desenvolvedores incluam conteúdo HTML avançado, como imagens, SVGs e layouts complexos, dentro de elementos <option>, mantendo a acessibilidade nativa do teclado e a integração de formulários. Use esse padrão para substituir componentes de seleção pesados e personalizados por elementos padrão e nativos.

Como implementar

Para implementar um seletor de mídia avançada usando a API Customizable Select:

  1. Ativar os estilos de base: aplique appearance: base-select ao elemento <select> e ao seletor interno usando o pseudoelemento ::picker(select). Isso muda o analisador HTML do navegador para o conteúdo dentro de <select>.
  2. Defina o conteúdo do botão: use elementos padrão <button> e <selectedcontent> dentro de <select> para definir o que é mostrado quando o seletor é fechado. O elemento <selectedcontent> espelha automaticamente o conteúdo da opção selecionada. Isso é necessário se você quiser mostrar o conteúdo avançado selecionado no botão.
  3. Usar conteúdo avançado dentro de opções: agora é possível colocar imagens, SVGs e outras tags HTML dentro de tags <option>. Antes dessa API, o navegador removia as tags de <option> e renderizava apenas texto simples.
  4. Estilizar o popover: estilize a lista de opções suspensas segmentando o pseudoelemento ::picker(select). Ele é renderizado na camada superior, o que significa que você não precisa lutar com z-index. As opções são posicionadas usando a API Anchor Positioning de forma nativa.

Exemplo de código: seletor de papéis avançado

<label for="role-picker">Select your role</label>
<select class="custom-select" name="role" id="role-picker">
  <button>
    <selectedcontent></selectedcontent> <!-- Mirrors the selected option's content automatically so you do not need JS to update the button -->
  </button>

  <!-- Define concise aria-label values on options whose mirrored rich content would otherwise read awkwardly as a concatenated string -->
  <option value="frontend" aria-label="Frontend Developer">
    <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
    </svg>
    <div class="option-text">
      <span class="option-title">Frontend Developer</span>
      <span class="option-desc">React, Vue, CSS</span>
    </div>
  </option>

  <option value="backend" aria-label="Backend Developer">
    <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <rect x="2" y="12" width="20" height="14" rx="2" ry="2"></rect>
    </svg>
    <div class="option-text">
      <span class="option-title">Backend Developer</span>
      <span class="option-desc">Node.js, Python</span>
    </div>
  </option>
</select>
select.custom-select,
select.custom-select::picker(select) {
  appearance: base-select; /* MUST opt-in both the select and its picker to enable the customizable state, otherwise browser standard rendering applies */
}

select.custom-select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 12px;
  background-color: #1e293b;
  border: 1px solid #334155;
  border-radius: 8px;
  color: #f1f5f9;
  cursor: pointer;
}

select.custom-select::picker(select) {
  background-color: #0f172a;
  border: 1px solid #334155;
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
  width: anchor-size(width); /* Uses Anchor Positioning API to keep the dropdown precisely aligned to the button trigger width */
}

select.custom-select option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
}

select.custom-select option:hover {
  background-color: #1e293b;
}

/* Remove standard OS checkmark for base-select */
select.custom-select option::before {
  display: none;
}

/* MANDATORY: Provide multiple visual indicators (e.g., prominent background color and bold title font) to communicate the checked state cleanly */
select.custom-select option:checked {
  background-color: #3b82f6;
  color: #ffffff;
}
select.custom-select option:checked .option-title {
  font-weight: 700;
}

Implementação estratégica e práticas recomendadas

  • USE appearance: base-select quando precisar de layouts de opções complexos (ícones, descrições, imagens) que antes só eram possíveis com frameworks de IU pesados do JavaScript.
  • NÃO use elementos ad hoc se notar atrasos no desempenho. O navegador lida com o foco do teclado nativamente.
  • CONSIDERE a renderização da camada superior. O seletor é renderizado na camada superior, o que significa que ele substitui o z-index relativo do conteúdo da página.
  • OCULTE detalhes secundários (como descrições) no estado do botão se eles ocuparem muito espaço, estilizando .custom-select selectedcontent .option-desc { display: none; }.
  • 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.

Estratégias 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.
  }
});