La propriété CSS appearance: base-select permet de styliser un élément <select> et son sélecteur (via ::picker(select)) comme des éléments HTML standards.
En combinant ces éléments avec des techniques de mise en page CSS modernes telles que Grid ou Flexbox, vous pouvez vous affranchir de la liste verticale empilée traditionnelle et créer une expérience visuelle riche sans JS personnalisé.
Par exemple, vous pouvez utiliser display: grid pour styliser les options sous forme de grille de fiches en 2D.
Cette option est idéale pour les sélecteurs de couleur, les sélecteurs d'emoji ou les variantes de produits, pour lesquels un menu visuel riche est plus efficace qu'une liste.
Implémentation
1. Activer le style de base
Appliquez appearance: base-select à l'élément <select> et à son pseudo-élément de sélecteur interne select::picker(select) :
select.grid-picker {
&, &::picker(select) {
appearance: base-select;
}
}
2. Appliquer un style au conteneur du sélecteur
Ciblez select:open::picker(select) et appliquez display: grid (ou display: flex). Définissez les colonnes et les espaces comme vous le feriez pour n'importe quel conteneur.
select.grid-picker:open::picker(select) {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
IMPORTANT : Le :open est obligatoire. Si vous appliquez un style à select::picker(select), le sélecteur sera visible même lorsque la sélection est fermée.
3. Appliquer un style aux options
Ciblez les éléments <option> pour les styliser en tant qu'éléments de grille ou cartes.
Vous pouvez y ajouter des images, des SVG ou des mises en page complexes.
/* Style options as grid cards */
.grid-picker option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* padding, border-radius, background, etc. */
}
4. Appliquer un style à l'option sélectionnée
Pour appliquer un style à l'option sélectionnée, utilisez option:checked. Exemple :
.grid-picker option:checked {
position: relative;
border: 2px solid var(--primary-color);
background-color: oklab(from var(--primary-color) l a b / 8%);
}
Comme pour toute commande d'interface utilisateur, ne communiquez pas l'état uniquement par la couleur. Utilisez plusieurs indicateurs visuels (par exemple, des couleurs, l'épaisseur de la bordure, une coche) pour indiquer l'option sélectionnée.
5. (Facultatif) Styliser la coche de l'option sélectionnée
Par défaut, le navigateur affiche une coche comme premier enfant de chaque <option>.
Il se comporte comme un ::before avec content: "✓" / "", affiché avant un ::before fourni par l'auteur, le cas échéant.
Si l'option n'est pas sélectionnée, la coche est masquée via visibility: hidden.
Vous pouvez le styliser à l'aide du pseudo-élément ::checkmark ou le masquer entièrement si vous souhaitez indiquer la sélection uniquement par le style ou utiliser un indicateur personnalisé.
Par exemple, pour le placer en haut à droite de l'option, vous pouvez procéder comme suit :
select.grid-picker {
> option::checkmark {
/* Hide the checkmark for unselected options */
display: none;
}
> option:checked {
position: relative;
&::checkmark {
position: absolute;
inset-block-start: -.4em;
inset-inline-end: -.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-inline-size: 1.6em;
aspect-ratio: 1;
padding-right: 0.12em;
padding-left: 0.05em;
border-radius: 50%;
background: var(--primary-color);
color: white;
font-size: 70%;
font-weight: bold;
}
}
}
Conseil : Lorsque vous dessinez un conteneur (par exemple, un cercle) autour de la coche, déplacez-le légèrement vers le haut (comme nous l'avons fait ici avec padding-bottom et padding-left) pour un centrage optique qui tient compte de sa forme.
N'utilisez PAS padding-inline-start ni padding-block-end, car ils changeront en fonction du mode d'écriture, contrairement à la forme de la coche.
6. (Facultatif) Personnalisez l'affichage de l'option sélectionnée.
Par défaut, le <select> affiche une version textuelle de l'option sélectionnée.
Pour afficher du contenu enrichi pour la valeur sélectionnée, utilisez <selectedcontent> dans un <button> et/ou pour afficher l'option sélectionnée différemment (par exemple, pour masquer certains détails).
N'utilisez PAS <selectedcontent> sans l'entourer de <button>, car <selectedcontent> sera simplement intégré au sélecteur, comme n'importe quel autre élément de contenu.
Bonnes pratiques
- Tester le comportement de la mise en page La définition de
appearance: base-selectsupprime 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 attributnameet 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
valuestandards sur les options pour vous assurer que l'envoi du formulaire fonctionne exactement comme avant.
Accessibilité
Un <select> natif ne gère que la navigation linéaire avec les flèches vers le haut et vers le bas. Les flèches vers la gauche et vers la droite n'ont donc aucun effet dans le sélecteur ouvert, et les flèches vers le haut et vers le bas permettent toujours de parcourir les éléments de manière séquentielle, indépendamment des lignes et des colonnes.
Pour implémenter une navigation spatiale qui suit la grille, vous pouvez intercepter keydown et diriger la sélection vers l'option la plus proche dans la direction de la touche enfoncée (par exemple, à l'aide de getBoundingClientRect()) lorsque les touches fléchées sont utilisées.
Pour déterminer l'élément suivant/précédent approprié, utilisez des méthodes DOM qui opèrent sur la géométrie réelle (par exemple, getBoundingClientRect()) plutôt que d'analyser des chaînes CSS telles que grid-template-columns ou grid-column, qui peuvent être fragiles et sujettes aux erreurs pour les grilles complexes, les sous-grilles, les mises en page de droite à gauche et d'autres scénarios de mise en page.
Cela dit, ne compliquez pas inutilement les choses. Utilisez les invariants que vous savez vrais concernant la mise en page pour simplifier cette logique.
Remarque : Cela ne concerne que la navigation au clavier. <select> et son ::picker() conservent leurs rôles par défaut (menu ou listbox), qui ne peuvent pas être remplacés et ne peuvent pas contenir d'élément avec role="grid".
L'imbrication d'un wrapper avec role="grid" dans le sélecteur fonctionne en pratique, mais n'est pas valide et peut surprendre les utilisateurs de TA.
Dans certains cas, l'utilisation de <optgroup> pour regrouper les options peut produire une bonne expérience utilisateur avec un lecteur d'écran sans role="grid".
Sinon, il peut être nécessaire de passer à une solution personnalisée utilisant l'API Popover, le positionnement d'ancrage et role="grid" pour obtenir l'expérience utilisateur souhaitée pour les utilisateurs de lecteurs d'écran.
Stratégie 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.
}
});