Sélecteur de sélection animé

Fonctionnalités Web : <select> personnalisable

L'API de sélection personnalisable offre un moyen déclaratif basé sur CSS d'animer les éléments <select> et leurs sélecteurs déroulants. En combinant appearance: base-select avec des techniques d'animation CSS modernes, telles que @starting-style et le comportement de transition allow-discrete, vous pouvez créer des transitions d'interface utilisateur fluides et de qualité pour les éléments de la couche supérieure sans dépendre de bibliothèques JavaScript lourdes.

Auparavant, il était impossible d'animer les menus déroulants de sélection natifs, car leur UI était affichée en dehors des contraintes de la fenêtre d'affichage accessible. Avec appearance: base-select, le sélecteur peut être stylisé et animé comme n'importe quel autre élément de page.

Implémentation

Pour implémenter un sélecteur animé :

  1. Activer la personnalisation : appliquez appearance: base-select à l'élément <select> et au pseudo-élément ::picker(select).
  2. Activer les transitions de dimensionnement automatique (facultatif) : définissez interpolate-size: allow-keywords (généralement sur :root) pour permettre au navigateur d'effectuer une transition entre des valeurs de métriques discrètes telles que height: auto et height: 0.
  3. Animer le conteneur de la couche supérieure : appliquez des styles d'entrée/de sortie standards à ::picker(select). Pour vous assurer que la transition d'opacité fonctionne lors du passage de display: none à display: block, vous devez utiliser transition-behavior: allow-discrete (souvent écrit de manière intégrée sous la forme transition: display 0.3s allow-discrete).
  4. Accrochez-vous à l'état d'ouverture avec @starting-style : utilisez @starting-style pour définir les styles de référence que le navigateur doit calculer avant le début de la transition. Par exemple, si vous souhaitez qu'il apparaisse en fondu, définissez l'opacité sur 0 dans le bloc @starting-style.
  5. Faire pivoter l'icône : utilisez des sélecteurs de pseudo-éléments actifs ou de sélection, comme :open::picker-icon, pour appliquer des transitions (telles qu'une rotation ou une translation) à l'indicateur en forme de flèche.

Exemple de code : Smooth Select Scale and Fade

L'exemple suivant illustre une sélection personnalisée stylisée avec des animations de page standards pour le conteneur du sélecteur.

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

Implémentation stratégique et bonnes pratiques

  • À FAIRE : Utilisez @starting-style lorsque vous avez besoin que les animations se déclenchent exactement au moment où un élément passe de display: none à visible.
  • N'UTILISEZ PAS le verrouillage du défilement ad hoc. Les éléments de la couche supérieure gérés par "base-select" doivent permettre des comportements de fermeture naturelle de l'arrière-plan.
  • À FAIRE : Vérifiez les préférences de réduction des animations. Encapsulez toujours les contraintes d'animation dans une requête média prefers-reduced-motion pour garantir des environnements accessibles aux personnes souffrant du mal des transports.
  • Tester le comportement de la mise en page La définition de appearance: base-select supprime le comportement par défaut du navigateur, qui consiste à dimensionner l'élément select en fonction de la largeur de l'option la plus longue. Vous devrez peut-être définir une largeur fixe ou utiliser des contraintes flex/grid pour éviter les décalages de mise en page.
  • Assurez-vous que votre <select> comporte un attribut name et un <label> associé. Cela garantit que, même avec une UI personnalisée, le composant reste accessible aux lecteurs d'écran et fonctionne correctement avec les envois de formulaires standards.
  • Utilisez les attributs value standards sur les options pour vous assurer que l'envoi du formulaire fonctionne exactement comme avant.

Stratégies de remplacement

Solutions de remplacement et compatibilité des navigateurs pour l'élément <select> personnalisable

Pour les navigateurs qui ne sont pas encore compatibles avec appearance: base-select, l'élément <select> se dégrade de manière fluide en un menu déroulant standard du système d'exploitation.

  • Contenu non textuel ignoré : les anciens navigateurs suppriment les balises HTML (comme <svg> ou <div>) à l'intérieur des balises <option> et n'affichent que les nœuds de texte. Assurez-vous que le contenu textuel de <option> est lisible et compréhensible en lui-même.
  • Gestion de la structure HTML : les analyseurs standards peuvent ignorer les balises <button> et <selectedcontent> dans <select> ou les traiter comme non valides. Aucun polyfill JavaScript lourd n'est strictement requis pour l'amélioration progressive si vous considérez le texte standard comme une solution de remplacement lisible.
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});