Selettore rich media (selezione personalizzabile)

Funzionalità web: <select> personalizzabile

L'elemento nativo <select> era storicamente difficile da formattare e poteva contenere solo opzioni di testo normale. La proprietà appearance: base-select offre un modo dichiarativo, solo CSS, per attivare uno stato personalizzabile per l'elemento <select>. In questo modo, gli sviluppatori possono includere contenuti HTML avanzati, come immagini, SVG e layout complessi, all'interno degli elementi <option>, mantenendo l'accessibilità della tastiera nativa e l'integrazione dei moduli. Utilizza questo pattern per sostituire i componenti di selezione personalizzati e pesanti con elementi nativi standard.

Modalità di implementazione

Per implementare un selettore di rich media utilizzando l'API Customizable Select:

  1. Attiva gli stili di base: applica appearance: base-select sia all'elemento <select> sia al selettore interno utilizzando lo pseudo-elemento ::picker(select). In questo modo, il parser HTML del browser viene modificato per i contenuti all'interno di <select>.
  2. Definisci i contenuti del pulsante: utilizza gli elementi <button> e <selectedcontent> standard all'interno di <select> per definire cosa viene mostrato quando il selettore è chiuso. L'elemento <selectedcontent> rispecchia automaticamente i contenuti dell'opzione selezionata. Questo è obbligatorio se vuoi visualizzare i contenuti avanzati attualmente selezionati nel pulsante.
  3. Utilizza contenuti avanzati all'interno delle opzioni: ora puoi inserire immagini, SVG e altri tag HTML all'interno dei tag <option>. Prima di questa API, il browser rimuoveva i tag dai tag <option> e mostrava solo testo normale.
  4. Definisci lo stile del popover: definisci lo stile dell'elenco di opzioni a discesa scegliendo come target lo pseudo-elemento ::picker(select). Viene visualizzato nel livello superiore, il che significa che non devi lottare con z-index. Le opzioni vengono posizionate utilizzando l'API Anchor Positioning in modo nativo.

Esempio di codice: selettore di ruoli avanzato

<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;
}

Implementazione strategica e best practice

  • SÌ: utilizza appearance: base-select quando hai bisogno di layout di opzioni complessi (icone, descrizioni, immagini) che in precedenza erano possibili solo con framework UI JavaScript pesanti.
  • NON utilizzare elementi ad hoc se noti ritardi nelle prestazioni; il browser gestisce la selezione della tastiera in modo nativo.
  • SÌ: tieni conto del rendering del livello superiore. Il selettore viene visualizzato nel livello superiore, il che significa che esegue l'override della proprietà z-index relativa dei contenuti della pagina.
  • SÌ: nascondi i dettagli secondari (come le descrizioni) nello stato del pulsante se occupano troppo spazio, applicando lo stile .custom-select selectedcontent .option-desc { display: none; }.
  • Testa il comportamento del layout. L'impostazione appearance: base-select rimuove il comportamento predefinito del browser di ridimensionare l'elemento select in base alla larghezza dell'opzione più lunga. Potresti dover impostare una larghezza fissa o utilizzare vincoli flex/grid per evitare variazioni del layout.
  • Assicurati che il tuo <select> abbia un attributo name e un <label> associato. In questo modo, anche con una UI personalizzata, il componente rimane accessibile agli screen reader e funziona correttamente con gli invii di moduli standard.
  • Utilizza gli attributi value standard sulle opzioni per assicurarti che l'invio del modulo funzioni esattamente come prima.

Strategie di fallback

Opzioni di riserva e supporto dei browser per <select> personalizzabile

Per i browser che non supportano ancora appearance: base-select, l'elemento <select> si riduce correttamente a un menu a discesa del sistema operativo standard.

  • Contenuti non testuali ignorati: i browser meno recenti rimuovono i tag HTML (come <svg> o <div>) all'interno dei tag <option> ed eseguono il rendering solo dei nodi di testo. Assicurati che i contenuti testuali di <option> siano leggibili e significativi di per sé.
  • Gestione della struttura HTML: i parser standard potrebbero ignorare i tag <button> e <selectedcontent> all'interno di <select> o trattarli come non validi. Per il potenziamento progressivo non sono strettamente necessari polyfill JavaScript pesanti se consideri il testo standard come un fallback leggibile.
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});