Comment Nordhealth utilise-t-elle les propriétés personnalisées de ses composants Web ?

Avantages de l'utilisation de propriétés personnalisées dans les systèmes de conception et les bibliothèques de composants.

David Darnes
David Darnes

Je suis Dave, développeur front-end senior chez Nordhealth. Je travaille sur la conception et le développement de notre système de conception Nord, 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 liés au style des Web Components en utilisant les propriétés personnalisées CSS, et vous présenter certains des autres avantages de l'utilisation des 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 passe-partout tels que l'état, les styles à portée limitée, 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 épuré qui tire parti des fonctionnalités dont le navigateur dispose déjà.


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);
Composant Web écrit avec Lit.

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 le package JavaScript principal référencé dans la page, l'utilisation d'un composant Web est très similaire à celle d'un élément HTML natif. Le seul véritable signe révélateur qu'il ne s'agit pas d'un élément HTML natif est le trait d'union constant 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, les Web Components disposent d'un Shadow DOM. Le Shadow DOM est une arborescence de nœuds masquée dans un élément. Le meilleur moyen de visualiser cela est d'ouvrir l'inspecteur Web et d'activer l'option "Afficher l'arborescence Shadow DOM". Une fois cette opération effectuée, essayez de rechercher un élément d'entrée natif dans l'inspecteur. Vous aurez désormais la possibilité d'ouvrir cette entrée et d'afficher tous les éléments qu'elle contient. Vous pouvez même essayer avec l'un de nos Web Components. Essayez d'inspecter notre composant d'entrée personnalisé pour voir son Shadow DOM.

Le Shadow DOM inspecté dans les outils de développement.
Exemple de Shadow DOM dans un élément d'entrée de texte standard et dans notre composant Web d'entrée Nord.

L'un des avantages (ou inconvénients, selon votre point de vue) du Shadow DOM est l'encapsulation du style. Si vous écrivez du code CSS dans votre composant Web, ces styles ne peuvent pas s'étendre et affecter la page principale ou d'autres éléments. Ils sont entièrement contenus dans le composant. De plus, le 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 permet de garantir davantage que lorsqu'une personne utilise l'un de nos composants, il s'affichera comme prévu, quels que soient les styles appliqués à la page parente. Pour nous en assurer, 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;
  /* ... */
}
Code de modèle de composant appliqué à la racine fantôme ou au sélecteur d'hôte.

Toutefois, que se passe-t-il si une personne utilisant votre composant Web a une raison légitime de modifier certains styles ? Peut-être qu'une ligne de texte a besoin de 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 débloquer ces options de style ?

C'est là que les propriétés personnalisées CSS 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 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 tirets. Une fois votre propriété personnalisée déclarée, sa valeur peut être utilisée 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);
}
Exemple tiré de notre framework CSS montrant un jeton de conception en tant que propriété personnalisée et son utilisation sur une classe d'assistance.

En ce qui concerne l'héritage, toutes les propriétés personnalisées sont héritées, ce qui suit le comportement typique des propriétés et valeurs CSS classiques. 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 de propriétés personnalisées pour nos jetons de conception en les appliquant à l'élément racine via 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 traverser le Shadow DOM de nos Web Components et de donner aux développeurs un contrôle plus précis lors de la mise en forme de 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 aimons viser un code simple, mais très facile à gérer. Les jetons de conception que nous avons 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);
  /* ... */
}
Propriétés CSS personnalisées définies sur le sélecteur racine.

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 y 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);
  /* ... */
}
Propriétés personnalisées définies sur la racine fantôme du composant, puis utilisées dans les styles du composant. Des propriétés personnalisées de la liste des jetons de conception sont également utilisées.

Nous allons également abstraire 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 nous offrent deux avantages clés. Tout d'abord, cela signifie que nous pouvons être plus "secs" avec notre CSS, car cette valeur peut être appliquée à plusieurs propriétés à l'intérieur du 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);
}
La propriété personnalisée contextuelle de marge intérieure du groupe d'onglets est utilisée à plusieurs endroits dans le code du composant.

Deuxièmement, il 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 définissez le style d'un état de survol ou actif, ou dans ce cas, d'une variante.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Variation du composant d'onglet où la marge intérieure est modifiée à l'aide d'une seule mise à jour de propriété personnalisée plutôt que de plusieurs mises à jour.

Toutefois, l'avantage le plus intéressant 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="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
Utilisation du composant de groupe d'onglets sur la page et mise à jour de la propriété personnalisée de marge intérieure sur une taille plus grande.

L'exemple précédent montre l'un de nos composants Web avec une propriété personnalisée contextuelle modifiée via un sélecteur. Le résultat de toute cette approche est 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 avons la possibilité d'intercepter les styles appliqués par l'utilisateur. Si nous souhaitons 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

Au moment de la rédaction de cet article, 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 l'ensemble de notre équipe de développement puisse les comprendre et les exploiter. Nos composants sont empaquetés sur npm avec un fichier manifeste, qui contient toutes les informations les concernant. Nous consommons ensuite le fichier manifeste en tant que données lorsque notre site de documentation est déployé, ce qui est fait à 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. Actuellement, par exemple, si vous souhaitez ajuster la couleur de deux composants de séparateur, 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>
Deux instances de notre composant de séparateur qui nécessitent deux traitements de couleur différents. L'un est imbriqué dans une section que nous pouvons utiliser pour un sélecteur plus spécifique, mais nous devons cibler spécifiquement le séparateur.

La raison pour laquelle vous devez définir la valeur de la propriété personnalisée directement sur le composant est que nous les définissons sur le même élément via le sélecteur d'hôte du composant. Les jetons de conception globaux que nous utilisons directement dans le composant sont transmis sans être affectés par ce problème. Ils 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);
  /* ... */
}
CSS du Web Component de séparateur avec les propriétés personnalisées contextuelles ajustées de sorte que le CSS interne repose sur une propriété personnalisée privée, qui a été définie sur une propriété personnalisée publique avec une valeur de remplacement.

Définir nos propriétés personnalisées contextuelles de cette manière signifie que nous pouvons toujours faire tout ce que nous faisions auparavant, comme hériter des valeurs de jeton globales et réutiliser les valeurs dans le code de notre composant. Mais le composant héritera également gracieusement 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>
Les deux séparateurs sont de nouveau présents, mais cette fois, ils peuvent être recolorés en ajoutant la propriété personnalisée contextuelle du séparateur au sélecteur de section. Le séparateur l'héritera, ce qui produira un code plus clair et plus flexible.

Bien que l'on puisse affirmer 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 inquiétait. Lorsque nous en aurons l'occasion, nous nous attaquerons à 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.

Nous espérons que cet aperçu de la façon dont nous utilisons les Web Components avec les propriétés personnalisées CSS vous a été utile. N'hésitez pas à nous dire ce que vous en pensez. Si vous décidez d'utiliser l'une de ces méthodes dans votre travail, vous pouvez me retrouver sur Twitter @DavidDarnes. Vous pouvez également trouver Nordhealth @NordhealthHQ sur Twitter, ainsi que le reste de mon équipe, qui a travaillé dur pour mettre en place ce système de conception et exécuter les fonctionnalités mentionnées dans cet article : @Viljamis, @WickyNilliams et @eric_habich.

Image principale de Dan Cristian Pădureț