X

Stratégies 3+1 – Léa Verou


Temps de lecture: 4 minutes

Lors du développement de composants personnalisables, on souhaite souvent exposer divers paramètres du style en tant que propriétés personnalisées et former une sorte de API CSS. Ceci est encore sous-utilisé, mais il existe des bibliothèques, par exemple Shoelace, qui répertorient déjà les propriétés personnalisées aux côtés d’autres parties de l’API de chaque composant (même des parties CSS !).

Note: J’utilise “composant” ici au sens large, comme tout morceau réutilisable de HTML/CSS/JS, pas nécessairement un composant Web ou un composant de framework. Ce dont nous allons discuter s’applique aux morceaux réutilisables de HTML tout autant qu’aux composants Web “propres”.

Supposons que nous concevons un certain style de bouton, qui ressemble à ceci :

Nous voulons soutenir une --color propriété personnalisée pour créer des variations de couleur en définissant plusieurs éléments en interne :

.fancy-button {
	border: .1em solid var(--color);
	background: transparent;
	color: var(--color);
}

.fancy-button:hover {
	background: var(--color);
	color: white;
}

Notez qu’avec le code ci-dessus, si non --color est défini, les trois déclarations qui l’utilisent seront IACVT et nous obtiendrons ainsi un bouton de texte presque sans style sans arrière-plan au survol (transparent), pas de bordure au survol et la couleur de texte noire par défaut (canvastext pour être précis).

Ce n’est pas bon! Il est important que nous définissions des valeurs par défaut. Cependant, utiliser le paramètre de repli pour cela devient fastidieux, et WET :

.fancy-button {
	border: .1em solid var(--color, black);
	background: transparent;
	color: var(--color, black);
}

.fancy-button:hover {
	background: var(--color, black);
	color: white;
}

Pour éviter la répétition tout en s’assurant --color a toujours une valeur, beaucoup de gens font ceci :

.fancy-button {
	--color: black;
	border: .1em solid var(--color);
	background: transparent;
	color: var(--color);
}

.fancy-button:hover {
	background: var(--color);
	color: white;
}

Cependant, ce n’est pas idéal pour plusieurs raisons :

  • Cela signifie que les gens ne peuvent pas profiter de l’héritage pour définir --color sur un ancêtre.
  • Cela signifie que les utilisateurs doivent utiliser une spécificité qui remplace vos propres règles pour définir ces propriétés. Dans ce cas, cela ne peut être que 0,1,0mais si vos sélecteurs sont complexes, cela pourrait finir par être assez ennuyeux (et introduire des couplages étroits, car les développeurs ne devraient pas avoir besoin de savoir quels sont vos sélecteurs).

Si vous insistez pour emprunter cette voie, :where() peut être un outil utile pour réduire la spécificité de vos sélecteurs tout en ayant des critères de sélection aussi fins que vous le souhaitez. C’est aussi l’une des fonctionnalités que j’ai proposées pour CSS, donc je suis très fier qu’il soit désormais supporté partout. :where() ne résoudra pas le problème d’héritage, mais au moins cela résoudra le problème de spécificité.

Et si nous utilisions toujours le paramètre fallback et utilisions une variable pour le fallback ?

.fancy-button {
	--color-initial: black;
	border: .1em solid var(--color, var(--color-initial));
	background: transparent;
	color: var(--color, var(--color-initial));
}

.fancy-button:hover {
	background: var(--color, var(--color-initial));
	color: white;
}

Cela fonctionne, et cela a l’avantage que les gens pourraient même personnalisez votre valeur par défaut s’ils le souhaitent (bien que je ne puisse penser à aucun cas d’utilisation pour cela). Mais n’est-ce pas ainsi horriblement verbeux? Que pourrions-nous faire d’autre ?

Ma solution préférée est ce que j’appelle propriétés personnalisées pseudo-privées. Vous utilisez en interne une propriété différente de celle que vous exposez, qui est définie sur celle que vous exposez plus la solution de repli :

.fancy-button {
	--_color: var(--color, black);
	border: .1em solid var(--_color);
	background: transparent;
	color: var(--_color);
}

.fancy-button:hover {
	background: var(--_color);
	color: white;
}

J’ai tendance à utiliser le même nom précédé d’un trait de soulignement. Certaines personnes peuvent hésiter à l’idée de propriétés privées qui ne sont pas vraiment privées, mais je vous rappelle que nous avons fait cela dans JS pendant plus de 20 ans (nous n’avons eu de vraies propriétés privées qu’assez récemment).

Bonus : valeurs par défaut via l’enregistrement @property

Si @property est un jeu équitable (il n’est pris en charge que dans Chromium, mais de nos jours, il est toujours pris en charge dans 70% des navigateurs des utilisateurs – ce qui est un peu triste, mais c’est une autre discussion), vous pouvez également définir les valeurs par défaut de cette façon :

@property --color {
	syntax: "<color>";
	inherits: true;
	initial-value: black;
}

.fancy-button {
	border: .1em solid var(--color);
	background: transparent;
	color: var(--color);
}

.fancy-button:hover {
	background: var(--color);
	color: white;
}

L’enregistrement de votre propriété présente plusieurs avantages (par exemple, cela le rend animable), mais si vous l’enregistrez uniquement dans le but de définir une valeur par défaut, cette méthode présente plusieurs inconvénients :

  • L’enregistrement de la propriété est mondial. Les propriétés personnalisées de votre composant peuvent entrer en conflit avec les propriétés personnalisées de la page hôte, ce qui n’est pas très bien. Les conséquences peuvent être très graves, car @property échoue silencieusement et le dernier l’emporte, vous pouvez donc simplement obtenir la valeur initiale de la propriété de la page hôte. Dans ce cas, cela pourrait très probablement être transparent, avec des résultats terribles. Et si votre déclaration est la dernière et que vous obtenez votre propre propriété enregistrée, cela signifie que le reste de la page recevra également la vôtre, avec des résultats tout aussi potentiellement terribles.
  • Avec cette méthode, vous ne pouvez pas définir différentes valeurs initiales par déclaration (bien que vous ne le souhaitiez généralement pas).
  • Toutes les syntaxes de propriétés personnalisées ne peuvent pas être décrites via @property encore.

Bonus : commutateur CSS pur à case unique personnalisable

Juste pour le lulz, j’ai fait un interrupteur (style vaguement inspiré de l’interrupteur Shoelace) qui est juste un habitué <input type=checkbox> avec une API de propriété personnalisée assez complète :

Il utilise l’approche des propriétés pseudo-privées. Notez qu’un autre bonus de cette méthode est qu’il y a une petite auto-documentation sur l’API de propriété personnalisée du composant, avant même que toute documentation réelle ne soit écrite.

En passant, des choses comme ce commutateur me font souhaiter qu’il soit possible de créer des composants Web qui sous-classent des éléments existants. Il existe une solution — quelque peu maladroite — avec le is attribut, mais Apple le bloque. L’alternative consiste à utiliser un composant Web avec ElementInternals pour le rendre associé au formulaire et accessible et refléter toutes les méthodes et propriétés de case à cocher, mais c’est beaucoup trop lourd et sujet à la rupture à l’avenir, car les cases à cocher natives ajoutent plus de méthodes. Il y a aussi un polyfill, mais pour un simple interrupteur, c’est peut-être un peu exagéré. Nous ne devrions vraiment pas avoir besoin de refléter minutieusement les éléments natifs pour les sous-classer…

Vous avez aimé cet article et souhaitez en savoir plus ? J’enseigne des cours sur l’exploitation du plein potentiel des propriétés personnalisées CSS. Vous pouvez regarder mon cours Frontend Masters Dynamic CSS (actuellement en production), ou assister à mon prochain atelier Smashing.