Les avantages de l'utilisation de propriétés personnalisées dans les systèmes de conception et les bibliothèques de composants.
Je m'appelle Dave et je suis développeur front-end senior chez Nordhealth. Je travaille sur la conception et le développement de notre système de conception Nord, ce qui inclut la création de composants Web pour notre bibliothèque de composants. Je voulais vous expliquer comment nous avons résolu les problèmes de style des composants Web en utilisant des propriétés CSS personnalisées, ainsi que d'autres avantages de l'utilisation de propriétés personnalisées dans les systèmes de conception et les bibliothèques de composants.
Comment nous créons des composants Web
Pour créer nos composants Web, nous utilisons Lit, une bibliothèque qui fournit de nombreux codes récurrents tels que l'état, les styles limités, les modèles, etc. Lit est non seulement léger, mais il est également basé sur des API JavaScript natives. Cela signifie que nous pouvons fournir un bundle de code léger qui exploite les fonctionnalités déjà présentes dans le navigateur.
import {html, css, LitElement} from 'lit';
export class SimpleGreeting extends LitElement {
static styles = css`:host { color: blue; font-family: sans-serif; }`;
static properties = {
name: {type: String},
};
constructor() {
super();
this.name = 'there';
}
render() {
return html`Hey
${this.name}, welcome to Web Components!`;
}
}
customElements.define('simple-greeting', SimpleGreeting);
Mais l'aspect le plus intéressant des composants Web est qu'ils fonctionnent avec presque tous les frameworks JavaScript existants, voire sans aucun framework. Une fois que le package JavaScript principal est référencé dans la page, l'utilisation d'un composant Web est très semblable à celle d'un élément HTML natif. Le seul signe révélateur qu'il ne s'agit pas d'un élément HTML natif est le trait d'union cohérent dans les balises, qui est une norme pour indiquer au navigateur qu'il s'agit d'un composant Web.
Encapsulation du style Shadow DOM
Tout comme les éléments HTML natifs ont un Shadow DOM, les composants Web en ont un aussi. Le Shadow DOM est une arborescence de nœuds masquée dans un élément. Pour mieux visualiser cela, ouvrez l'inspecteur Web et activez l'option "Show Shadow DOM tree" (Afficher l'arborescence Shadow DOM). Une fois cette opération effectuée, essayez d'examiner un élément d'entrée natif dans l'inspecteur. Vous pourrez alors ouvrir cet élément d'entrée et voir tous les éléments qu'il contient. Vous pouvez même essayer avec l'un de nos composants Web. Essayez d'inspecter notre composant de saisie personnalisé pour voir son Shadow DOM.
L'un des avantages (ou inconvénients, selon votre point de vue) du Shadow DOM est l'encapsulation des styles. Si vous écrivez du code CSS dans votre composant Web, ces styles ne peuvent pas s'échapper et affecter la page principale ou d'autres éléments. Ils sont entièrement contenus dans le composant. De plus, le code CSS écrit pour la page principale ou un composant Web parent ne peut pas s'infiltrer dans votre composant Web.
Cette encapsulation des styles est un avantage dans notre bibliothèque de composants. Cela nous donne une meilleure garantie que lorsqu'un utilisateur utilise l'un de nos composants, il s'affichera comme prévu, quels que soient les styles appliqués à la page parente. Pour plus de sécurité, nous ajoutons all: unset; à la racine, ou "hôte", de tous nos composants Web.
:host {
all: unset;
display: block;
box-sizing: border-box;
text-align: start;
/* ... */
}
Toutefois, que se passe-t-il si un utilisateur de votre composant Web a une raison légitime de modifier certains styles ? Peut-être qu'une ligne de texte nécessite plus de contraste en raison de son contexte, ou qu'une bordure doit être plus épaisse ? Si aucun style ne peut être appliqué à votre composant, comment pouvez-vous débloquer ces options de style ?
C'est là que les propriétés CSS personnalisées entrent en jeu.
Propriétés personnalisées CSS
Les propriétés personnalisées portent bien leur nom : ce sont des propriétés CSS que vous pouvez nommer entièrement vous-même et auxquelles vous pouvez appliquer la valeur de votre choix. La seule exigence est de les faire précéder de deux traits d'union. Une fois que vous avez déclaré votre propriété personnalisée, vous pouvez utiliser sa valeur dans votre CSS à l'aide de la fonction var().
:root {
--n-color-accent: rgb(53, 89, 199);
/* ... */
}
.n-color-accent-text {
color: var(--n-color-accent);
}
En ce qui concerne l'héritage, toutes les propriétés personnalisées sont héritées, ce qui correspond au comportement habituel des propriétés et valeurs CSS standards. Toute propriété personnalisée appliquée à un élément parent, ou à l'élément lui-même, peut être utilisée comme valeur sur d'autres propriétés. Nous utilisons beaucoup les propriétés personnalisées pour nos jetons de conception en les appliquant à l'élément racine à l'aide de notre framework CSS. Cela signifie que tous les éléments de la page peuvent utiliser ces valeurs de jeton, qu'il s'agisse d'un composant Web, d'une classe d'assistance CSS ou d'un développeur souhaitant extraire une valeur de notre liste de jetons.
Cette capacité à hériter des propriétés personnalisées, grâce à la fonction var(), nous permet de percer le Shadow DOM de nos composants Web et d'offrir aux développeurs un contrôle plus précis sur l'application de styles à nos composants.
Propriétés personnalisées dans un composant Web Nord
Chaque fois que nous développons un composant pour notre système de conception, nous adoptons une approche réfléchie pour son CSS. Nous visons un code léger, mais très facile à gérer. Les jetons de conception dont nous disposons sont définis en tant que propriétés personnalisées dans notre framework CSS principal sur l'élément racine.
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
Ces valeurs de jeton sont ensuite référencées dans nos composants. Dans certains cas, nous appliquerons la valeur directement à la propriété CSS, mais dans d'autres, nous définirons une nouvelle propriété personnalisée contextuelle et lui appliquerons la valeur.
:host {
--n-tab-group-padding: 0;
--n-tab-list-background: var(--n-color-background);
--n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
/* ... */
}
.n-tab-group-list {
box-shadow: var(--n-tab-list-border);
background-color: var(--n-tab-list-background);
gap: var(--n-space-s);
/* ... */
}
Nous allons également extraire certaines valeurs spécifiques au composant, mais qui ne figurent pas dans nos jetons, et les transformer en propriété personnalisée contextuelle. Les propriétés personnalisées contextuelles au composant offrent deux avantages clés. Tout d'abord, cela signifie que nous pouvons être plus "DRY" (Don't Repeat Yourself) avec notre CSS, car cette valeur peut être appliquée à plusieurs propriétés dans le composant.
.n-tab-group-list::before {
/* ... */
padding-inline-start: var(--n-tab-group-padding);
}
.n-tab-group-list::after {
/* ... */
padding-inline-end: var(--n-tab-group-padding);
}
Ensuite, cela permet de modifier l'état et les variantes des composants de manière très propre. Seule la propriété personnalisée doit être modifiée pour mettre à jour toutes ces propriétés lorsque, par exemple, vous appliquez un style à un état de survol ou actif, ou, dans ce cas, à une variante.
:host([padding="l"]) {
--n-tab-group-padding: var(--n-space-l);
}
Mais l'avantage le plus important est que lorsque nous définissons ces propriétés personnalisées contextuelles sur un composant, nous créons une sorte d'API CSS personnalisée pour chacun de nos composants, qui peut être exploitée par l'utilisateur de ce composant.
<nord-tab-group label="Title">
<!-- ... -->
</nord-tab-group>
<style>
nord-tab-group {
--n-tab-group-padding: var(--n-space-xl);
}
</style>
L'exemple précédent montre l'un de nos composants Web avec une propriété personnalisée contextuelle modifiée à l'aide d'un sélecteur. Cette approche permet d'obtenir un composant qui offre à l'utilisateur une flexibilité de style suffisante tout en gardant la plupart des styles réels sous contrôle. De plus, en tant que développeurs de composants, nous pouvons intercepter les styles appliqués par l'utilisateur. Si nous voulons ajuster ou étendre l'une de ces propriétés, nous pouvons le faire sans que l'utilisateur ait besoin de modifier son code.
Nous trouvons cette approche extrêmement efficace, non seulement pour nous en tant que créateurs des composants de notre système de conception, mais aussi pour notre équipe de développement lorsqu'elle utilise ces composants dans nos produits.
Aller plus loin avec les propriétés personnalisées
À l'heure où nous écrivons ces lignes, nous ne révélons pas ces propriétés personnalisées contextuelles dans notre documentation. Toutefois, nous prévoyons de le faire afin que notre équipe de développement puisse comprendre et exploiter ces propriétés. Nos composants sont empaquetés sur npm avec un fichier manifeste, qui contient toutes les informations les concernant. Nous utilisons ensuite le fichier manifeste comme données lorsque notre site de documentation est déployé, à l'aide d'Eleventy et de sa fonctionnalité de données globales. Nous prévoyons d'inclure ces propriétés personnalisées contextuelles dans ce fichier de données du fichier manifeste.
Nous souhaitons également améliorer la façon dont ces propriétés personnalisées contextuelles héritent des valeurs. Par exemple, si vous souhaitez ajuster la couleur de deux composants de séparation, vous devez cibler spécifiquement ces deux composants avec des sélecteurs ou appliquer la propriété personnalisée directement sur l'élément avec l'attribut de style. Cela peut sembler correct, mais il serait plus utile que le développeur puisse définir ces styles sur un élément conteneur ou même au niveau racine.
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
}
section nord-divider {
--n-divider-color: var(--n-color-status-success);
}
</style>
Vous devez définir la valeur de la propriété personnalisée directement sur le composant, car nous les définissons sur le même élément à l'aide du sélecteur d'hôte du composant. Les jetons de conception globaux que nous utilisons directement dans le composant sont transmis directement, sans être affectés par ce problème, et peuvent même être interceptés sur les éléments parents. Comment tirer le meilleur parti de l'association de ces deux approches ?
Propriétés personnalisées privées et publiques
Les propriétés personnalisées privées ont été créées par Lea Verou. Il s'agit d'une propriété personnalisée "privée" contextuelle sur le composant lui-même, mais définie sur une propriété personnalisée "publique" avec une valeur de remplacement.
:host {
--_n-divider-color: var(--n-divider-color, var(--n-color-border));
--_n-divider-size: var(--n-divider-size, 1px);
}
.n-divider {
border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
/* ... */
}
En définissant nos propriétés personnalisées contextuelles de cette manière, nous pouvons toujours faire tout ce que nous faisions auparavant, comme hériter des valeurs de jetons globaux et réutiliser des valeurs dans l'ensemble du code de notre composant. Cependant, le composant héritera également de manière fluide des nouvelles définitions de cette propriété sur lui-même ou sur tout élément parent.
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
--n-divider-color: var(--n-color-status-success);
}
</style>
Même si l'on peut arguer que cette méthode n'est pas vraiment "privée", nous pensons qu'il s'agit d'une solution plutôt élégante à un problème qui nous préoccupait. Dès que nous en aurons l'occasion, nous nous pencherons sur ce problème dans nos composants afin que notre équipe de développement ait plus de contrôle sur l'utilisation des composants, tout en bénéficiant des garde-fous que nous avons mis en place.
J'espère que cet aperçu de la façon dont nous utilisons les composants Web avec les propriétés personnalisées CSS vous a été utile. Dites-nous ce que vous en pensez. Si vous décidez d'utiliser l'une de ces méthodes dans votre propre travail, vous pouvez me contacter sur Twitter à l'adresse @DavidDarnes. Vous pouvez également retrouver Nordhealth sur Twitter (@NordhealthHQ), ainsi que le reste de mon équipe, qui a travaillé dur pour créer ce système de conception et implémenter les fonctionnalités mentionnées dans ce document : @Viljamis, @WickyNilliams et @eric_habich.