Selettore di selezione animato

Funzionalità web: <select> personalizzabile

L'API select personalizzabile offre un modo dichiarativo basato su CSS per animare gli elementi <select> e i relativi selettori a discesa. Combinando appearance: base-select con le moderne tecniche di animazione CSS, come @starting-style e il comportamento di transizione allow-discrete, puoi creare transizioni fluide e di alta qualità per gli elementi del livello superiore senza dover ricorrere a librerie JavaScript pesanti.

In precedenza, l'animazione dei menu a discesa di selezione nativi era impossibile perché la loro UI veniva visualizzata al di fuori dei vincoli della visualizzazione accessibile. Con appearance: base-select, il selettore diventa personalizzabile e animabile come qualsiasi altro elemento di pagina.

Modalità di implementazione

Per implementare un selettore animato:

  1. Attiva la personalizzazione: applica appearance: base-select sia all'elemento <select> sia allo pseudo-elemento ::picker(select).
  2. Attiva le transizioni di ridimensionamento automatico (facoltativo): definisci interpolate-size: allow-keywords (di solito su :root) per consentire al browser di eseguire la transizione tra valori di metriche discreti come height: auto e height: 0.
  3. Anima il container del livello superiore: applica gli stili di entrata/uscita standard a ::picker(select). Per assicurarti che la transizione dell'opacità funzioni quando passi da display: none a display: block, devi utilizzare transition-behavior: allow-discrete (spesso scritto in linea come transition: display 0.3s allow-discrete).
  4. Agganciati allo stato di apertura con @starting-style: utilizza @starting-style per definire gli stili di base che il browser deve calcolare prima dell'inizio della transizione. Ad esempio, se vuoi che l'elemento appaia in dissolvenza, imposta l'opacità su 0 all'interno del blocco @starting-style.
  5. Ruota l'icona: utilizza i selettori di stato attivo o attivo dello pseudo-elemento come :open::picker-icon per applicare transizioni (come rotazione o traslazione) all'indicatore a forma di freccia.

Esempio di codice: Smooth Select Scale and Fade

L'esempio seguente mostra un elemento select personalizzato con stile con animazioni di pagina standard per il contenitore del selettore.

<!-- Always use a <label> linked via 'for' to the select for accessibility -->
<label for="theme-select">Visual Theme</label>
<select id="theme-select" class="animated-select" name="theme">
  <!-- The <button> inside <select> becomes the visible trigger when appearance: base-select is used -->
  <button>
    <!-- <selectedcontent> automatically displays the content of the chosen <option> -->
    <selectedcontent></selectedcontent>
  </button>
  <option value="system">
    <!-- MANDATORY: Decorative inline SVGs MUST set aria-hidden="true" to prevent redundant screen reader announcement -->
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
      <line x1="8" y1="21" x2="16" y2="21"></line>
      <line x1="12" y1="17" x2="12" y2="21"></line>
    </svg>
    System Default
  </option>
  <option value="light">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="5"></circle>
      <line x1="12" y1="1" x2="12" y2="3"></line>
      <line x1="12" y1="21" x2="12" y2="23"></line>
      <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
      <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
      <line x1="1" y1="12" x2="3" y2="12"></line>
      <line x1="21" y1="12" x2="23" y2="12"></line>
      <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
      <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
    </svg>
    Light UI
  </option>
</select>
/* Opt-in to customizable select */
.animated-select,
.animated-select::picker(select) {
  appearance: base-select;
}

/* Enable auto-keyword transitions (usually set globally at :root) */
:root {
  interpolate-size: allow-keywords;
}

/* Style the visible trigger and icon rotation */
.animated-select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.875rem 1rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.animated-select::picker-icon {
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.animated-select:open::picker-icon {
  transform: rotate(180deg);
}

/*
 * The Picker Container
 * Uses top-layer animations with `allow-discrete` visibility hooks
 */
.animated-select::picker(select) {
  background: white;
  border-radius: 12px;
  box-shadow: 0 10px 25px -3px rgba(0,0,0,0.1);
  padding: 0.5rem;
  margin-top: 0.25rem;
  width: anchor-size(width);
  overflow: hidden;

  /* The crucial transition setting for popover animations */
  transition:
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete,
    opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  opacity: 0;
  height: 0;
}

/* Open State */
.animated-select:open::picker(select) {
  opacity: 1;
  height: auto;
}

/* @starting-style to hook the transition on initial popover open */
@starting-style {
  .animated-select:open::picker(select) {
    opacity: 0;
    height: 0;
  }
}

/* Support for SVG inside Options and Selected Content */
.animated-select option svg,
.animated-select selectedcontent svg {
  flex-shrink: 0; /* Prevent icons from shrinking */
  width: 1.25rem;
  height: 1.25rem;
}

/* MANDATORY: Provide multiple indicators (e.g. bold font and distinct background) for the checked state to avoid color-only state communication */
.animated-select option:checked {
  font-weight: 700;
  background-color: #f1f5f9;
}

/* Ensure copy-paste safety for users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  .animated-select::picker(select),
  .animated-select::picker-icon {
    transition: none !important;
  }
}

Implementazione strategica e best practice

  • SÌ: utilizza @starting-style quando hai bisogno di animazioni che si attivino esattamente quando un elemento passa da display: none a visibile.
  • NON utilizzare il blocco dello scorrimento ad hoc. Gli elementi del livello superiore gestiti da "base-select" devono consentire i comportamenti di chiusura naturale dello sfondo.
  • SÌ: verifica le preferenze di movimento ridotto. Inserisci sempre i vincoli di animazione in una query supporti prefers-reduced-motion per garantire ambienti accessibili per le persone che soffrono di cinetosi.
  • 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 riserva

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