12k
All articles

Styliser une liste déroulante `<select>` avec base-select

Stylisez les menus déroulants select natifs avec appearance: base-select. Découvrez le picker, les options, les icônes, l’accessibilité, la compatibilité et les limites.

OpenReplay Team
OpenReplay Team
Styliser une liste déroulante `<select>` avec base-select

Appliquer appearance: base-select à un <select> et à son pseudo-élément ::picker(select) permet de styliser l’ensemble du contrôle en CSS : le bouton, la liste déroulante, la flèche, la coche et chacune des options.

De nombreuses équipes maintiennent un composant de liste déroulante personnalisé pour une seule raison : le <select> natif ne pouvait pas être stylisé conformément à la maquette. La gestion du clavier, la gestion du focus et le câblage ARIA sont venus s’y greffer par la force des choses. appearance: base-select supprime cette raison d’être.

Ce guide passe en revue l’ensemble des possibilités de stylisation, de l’activation jusqu’aux options enrichies, et détaille ce à quoi vous renoncez. Pour les techniques classiques avec appearance: none, l’article styling select elements with modern CSS offre une vue d’ensemble plus large, mais n’aborde base-select que brièvement. Les approches plus anciennes sont présentées dans styling select dropdowns without JavaScript.

Points clés

  • appearance: base-select doit être défini à la fois sur select et sur ::picker(select). Le définir uniquement sur le picker n’a aucun effet.
  • Le picker est un popover affiché dans la top layer, ancré au bouton du select via une référence d’ancrage implicite : inutile donc de recourir à z-index, à des portails ou à anchor-name.
  • <selectedcontent> se place dans un <button> qui est le premier enfant du <select>, et affiche un clone de l’option sélectionnée dans le contrôle fermé.
  • Chaque option doit contenir du texte : les options composées uniquement d’une icône ne donnent rien à annoncer aux lecteurs d’écran et s’affichent vides dans les navigateurs non compatibles.
  • Avec base-select, vous renoncez au sélecteur natif du système d’exploitation mobile, à l’affichage en dehors de la fenêtre du navigateur et au dimensionnement automatique en fonction de l’option la plus longue.

Le composant que vous pouvez supprimer

Un composant select personnalisé n’existe généralement que pour corriger l’apparence de l’élément <select> natif, et ce faisant, il doit prendre en charge de nombreux comportements. L’article de lancement de l’équipe Chrome indique qu’à partir de Chrome 135, vous pouvez styliser l’élément <select> en CSS, tout en conservant l’accessibilité et le comportement standard d’un contrôle natif. WebKit a suivi avec Safari 27.0, où le véritable <select> peut désormais être restylé de fond en comble. Dès lors qu’un navigateur prend en charge cette fonctionnalité, la principale raison de conserver une listbox à base de div disparaît, tout comme les comportements que vous deviez reconstruire pour la faire fonctionner.

Comment activer appearance: base-select ?

Pour l’activer, appliquez appearance: base-select à la fois au <select> et à son picker. Le guide MDN sur le select personnalisable explique que le select peut être activé seul, mais que le picker ne fonctionne que si le select l’est également. Les recommandations de WebKit sur le select personnalisable préconisent d’encapsuler ces améliorations dans @supports (appearance: base-select). Placez toutes les règles suivantes dans ce même bloc, afin que les navigateurs non compatibles affichent le select standard.

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
  /* all following rules go here */
}
ÉlémentSélecteurRemarques
Contrôle ferméselectLe bouton du select
Flèche::picker-iconAbsent de l’arbre d’accessibilité
État ouvert:openCorrespond tant que le picker est affiché
Liste déroulante::picker(select)Tout sauf le bouton en premier enfant
Chaque choixoptiondisplay: flex par défaut
Choix sélectionnéoption:checked
Marque de sélection::checkmarkAbsent de l’arbre d’accessibilité
Contenu à l’état ferméselectedcontentClone de l’option sélectionnée

Styliser le contrôle fermé

Le contrôle fermé correspond à l’élément select. La flèche est ::picker-icon, et :open correspond tant que le picker est affiché, ce qui permet de faire pivoter la flèche à l’ouverture du select.

select {
  border: 2px solid #d4d4d8;
  background: #f4f4f5;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #e4e4e7;
}

select::picker-icon {
  color: #71717a;
  transition: 0.4s rotate;
}

select:open::picker-icon {
  rotate: 180deg;
}

La transition appliquée à rotate fait pivoter la flèche en douceur à chaque ouverture et fermeture du picker.

Styliser la liste ouverte

La liste déroulante ouverte d’un select personnalisable est ::picker(select). Le navigateur y regroupe tout le contenu du <select>, à l’exception du bouton placé en premier. Les options sont des conteneurs flex par défaut. :checked cible l’option sélectionnée, et ::checkmark cible la coche qui l’accompagne.

::picker(select) {
  border: none;
  border-radius: 8px;
}

option {
  display: flex;
  gap: 16px;
  padding: 10px;
}

option:hover,
option:focus {
  background: #ede9fe;
}

option:checked {
  font-weight: bold;
}

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "✔";
}

La combinaison de order: 1 et margin-left: auto déplace la coche du début à la fin de la ligne.

Positionnement et animation

Dans un select personnalisable, la liste déroulante est un popover qui s’ouvre dans la top layer. Le navigateur la rattache lui-même au bouton du select grâce au positionnement par ancrage CSS (anchor positioning) : inutile donc d’utiliser un z-index, un portail ou de câbler un anchor-name. Le bouton et le picker entretiennent la même relation invoker/popover que celle décrite dans le guide des invoker commands pour les boutons et les popovers. Les styles par défaut repositionnent également le picker d’un autre côté s’il risque de déborder de l’écran, et vous pouvez définir votre propre position avec anchor() :

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

Pour faire apparaître et disparaître le picker en fondu, appliquez une transition entre ses états de popover :

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

:open::picker(select) {
  opacity: 1;
}

@starting-style {
  :open::picker(select) {
    opacity: 0;
  }
}

allow-discrete permet aux propriétés discrètes display et overlay de participer à la transition. Sans cela, le picker quitterait la top layer avant que le fondu de sortie ne soit visible. @starting-style définit l’opacité à partir de laquelle démarre le fondu d’entrée.

Options enrichies et selectedcontent

Placez <selectedcontent> dans un <button> qui est le premier enfant du <select>. Il affiche un clone du contenu de l’option sélectionnée dans le contrôle fermé, et vous pouvez le styliser indépendamment des options de la liste déroulante. Les options peuvent désormais contenir du balisage, comme des icônes et des images.

<label for="status">Status</label>
<select id="status" name="status">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="draft">
    <span class="icon" aria-hidden="true">📝</span>
    <span class="option-label">Draft</span>
  </option>
  <option value="review">
    <img class="icon" src="review.svg" alt="">
    <span class="option-label">In review</span>
  </option>
  <option value="published">
    <span class="icon" aria-hidden="true">✅</span>
    <span class="option-label">Published</span>
  </option>
</select>

MDN signale que les icônes peuvent augmenter la hauteur du bouton fermé et décaler la flèche. Vous pouvez les masquer uniquement à l’état fermé :

selectedcontent .icon {
  display: none;
}

Les options du picker ouvert conservent leurs icônes. Le bouton du select est inerte : les liens ou boutons qu’il contient ne peuvent pas recevoir le focus ni être cliqués individuellement, et l’ensemble se comporte comme un seul bouton. MDN souligne également qu’un contenu supplémentaire dans le bouton peut modifier la valeur du select annoncée par les lecteurs d’écran ; limitez donc le contenu du bouton à <selectedcontent>.

Que prend en charge le navigateur à votre place ?

Avec base-select, le navigateur gère tout ce qu’une liste déroulante personnalisée doit réimplémenter : l’interaction au clavier, le focus, l’empilement dans la top layer, la soumission de la valeur dans le formulaire et la sémantique pour les lecteurs d’écran. WebKit précise que la prise en charge du clavier et l’accessibilité sont intégrées, puisqu’il s’agit toujours du contrôle natif du navigateur. Le replay de sessions utilisateur sur une liste déroulante à base de div révèle souvent des bugs de clavier et de focus, précisément ceux qu’un select natif évite par conception. Pour mesurer la quantité de code qu’exige l’approche manuelle, consultez ce guide pour construire à la main une liste déroulante accessible.

Deux règles restent de mise :

  • Conservez du texte dans chaque option. La règle d’or de WebKit est que chaque option doit comporter un véritable texte, ou un attribut de texte accessible. Un texte masqué visuellement reste lu par les lecteurs d’écran. Les images décoratives reçoivent alt="" et laissent le libellé textuel porter le sens.
  • Considérez les icônes personnalisées comme décoratives. Les lecteurs d’écran ignorent tout ce que vous ajoutez via content sur ::checkmark et ::picker-icon : une flèche ou une coche personnalisée n’est donc jamais lue à voix haute.

Le code JavaScript existant qui lit la valeur du select continue de fonctionner. Si une option n’a pas d’attribut value, la valeur du select correspond au textContent de l’option sélectionnée, débarrassé des espaces superflus ; définissez donc toujours value lorsque les options contiennent un balisage enrichi.

Compatibilité, solution de repli et compromis

appearance: base-select est disponible dans Chrome 135 et versions ultérieures, dans les autres navigateurs basés sur Chromium comme Edge, ainsi que dans Safari 27.0. Firefox a commencé à l’intégrer derrière une préférence : la valeur appearance: base-select est arrivée dans Firefox 149 derrière un flag, mais elle n’est pas activée par défaut. MDN la classe toujours en disponibilité limitée ; consultez donc les tableaux de compatibilité des pages de référence MDN. Safari embarque ses propres styles base-select par défaut : des contrôles non stylisés n’auront donc pas le même rendu d’un navigateur à l’autre.

Les navigateurs qui ne prennent pas en charge base-select ignorent le balisage <button> et <selectedcontent>. Ils affichent un select ordinaire qui fonctionne normalement, chaque option étant réduite à son texte. Cette solution de repli n’a de sens que si le texte est présent.

L’article de Chrome énumère également trois comportements auxquels vous renoncez :

  • La liste ouverte reste confinée à la fenêtre du navigateur.
  • Sur smartphones et tablettes, vous obtenez le sélecteur du navigateur au lieu de celui du système d’exploitation.
  • La largeur du select ne s’adapte plus à son option la plus longue : définissez donc vous-même une width sur select, ::picker(select).

Base-select modifie aussi la façon dont le parseur HTML interprète le contenu d’un <select>. MDN avertit que cela peut entrer en conflit avec certains frameworks JavaScript : certains bloquent ces fonctionnalités, et avec d’autres, le rendu côté serveur peut casser l’hydratation. Testez la fonctionnalité dans votre framework avant de supprimer votre composant existant.

Conclusion

Un select natif avec appearance: base-select vous donne la maîtrise du style, tandis que le navigateur continue de gérer le clavier, le focus, l’empilement et l’accessibilité. Pour l’essayer, choisissez une liste déroulante personnalisée simple, reconstruisez-la dans un bloc @supports en plaçant du texte dans chaque option, puis vérifiez-la dans un navigateur qui ne prend pas en charge base-select. Si elle y fonctionne toujours comme un select standard, et si les compromis évoqués plus haut sont acceptables, vous pouvez supprimer le composant personnalisé.

FAQ

Quelle est la différence entre appearance: none et appearance: base-select sur un select ?

appearance: none se contente de supprimer le style natif du bouton fermé. La liste déroulante et ses options utilisent toujours le sélecteur du navigateur ou du système d'exploitation, et la plupart des propriétés CSS ne les atteignent pas. appearance: base-select, défini à la fois sur select et sur ::picker(select), permet en plus de styliser le picker, les options, la flèche et la coche, et autorise du HTML enrichi dans les options. Les navigateurs non compatibles l'ignorent et conservent le contrôle classique.

Comment détecter la prise en charge de base-select en JavaScript ?

Appelez CSS.supports('appearance', 'base-select'). Cette méthode renvoie true uniquement dans les navigateurs qui reconnaissent cette valeur. Elle vérifie la même chose que la condition CSS @supports (appearance: base-select) : vous pouvez donc l'utiliser pour décider de charger un composant de liste déroulante de repli ou d'ajouter une classe qui active les styles personnalisés. Privilégiez la détection de fonctionnalités plutôt que l'analyse du user agent, car chaque moteur a ajouté la prise en charge à un moment différent.

base-select fonctionne-t-il avec select multiple ou l'attribut size ?

Oui, en mode listbox. L'attribut multiple, ou une valeur de size supérieure à 1, affiche les options sous forme de boîte dans la page au lieu d'un bouton ouvrant une fenêtre contextuelle. Depuis Chrome 145, base-select fonctionne également dans ce mode, en sélection simple comme multiple. Il ne couvre pas encore la fenêtre contextuelle à sélection multiple obtenue avec multiple et size=1. Une listbox personnalisable n'a pas de picker : ::picker(select), ::picker-icon et selectedcontent ne s'appliquent donc pas. Consultez les tableaux de compatibilité de MDN pour connaître la prise en charge dans les autres moteurs.

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.