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

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

David Darnes
David Darnes

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 implique la création de composants Web pour notre bibliothèque de composants. J'aimerais vous expliquer comment nous avons résolu les problèmes liés au style des composants Web à l'aide des propriétés CSS personnalisées, et vous présenter certains des autres avantages liés à 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 une grande quantité de code récurrent comme l'état, les styles à champ d'application, les modèles, etc. Lit est non seulement léger, mais il est également basé sur des API JavaScript natives, ce qui 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);
Composant Web écrit avec Lit.

Mais le plus intéressant à propos des composants Web est qu'ils fonctionnent avec presque tous les frameworks JavaScript existants, voire avec aucun framework du tout. Une fois que le package JavaScript principal est référencé dans la page, l'utilisation d'un composant Web est très semblable à l'utilisation d'un élément HTML natif. Le seul signe indiquant qu'il ne s'agit pas d'un élément HTML natif est le trait d'union cohérent dans les balises. Il s'agit d'une norme indiquant au navigateur qu'il s'agit d'un composant Web.


// TODO: DevSite - Code sample removed as it used inline event handlers
Utiliser sur une page le composant Web créé ci-dessus

Encapsulation du style Shadow DOM

Tout comme les éléments HTML natifs, les composants Web utilisent un Shadow DOM. Le Shadow DOM est une arborescence cachée de nœuds au sein d'un élément. Pour visualiser cela, ouvrez votre Web Inspector 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 avez maintenant 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 composants Web : inspectez notre composant d'entrée personnalisé pour voir son Shadow DOM.

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

L'un des avantages (ou des inconvénients, selon votre vision) de Shadow DOM est l'encapsulation de style. Si vous écrivez du code CSS dans votre composant Web, ces styles ne peuvent pas fuir ni affecter la page principale ou les autres éléments. Ils sont entièrement contenus dans le composant. De plus, les codes CSS écrits pour la page principale ou un composant Web parent ne peuvent pas être divulgués dans votre composant Web.

Cette encapsulation de styles est un avantage de notre bibliothèque de composants. Cela nous donne plus de certitudes quant au fait 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;
  /* ... */
}
Certains composants de code récurrent sont appliqués à la racine fantôme ou au sélecteur d'hôte.

Cependant, que se passe-t-il si un utilisateur de votre composant Web a une raison légitime de modifier certains styles ? Peut-être une ligne de texte a-t-elle besoin de plus de contraste en raison de son contexte ou une bordure doit être plus épaisse ? Si aucun style ne peut être intégré à votre composant, comment pouvez-vous déverrouiller ces options de style ?

C'est là qu'interviennent les propriétés CSS personnalisées.

Propriétés CSS personnalisées

Les propriétés personnalisées sont nommées de manière très appropriée. Ce sont des propriétés CSS que vous pouvez vous-même nommer vous-même et appliquer la valeur nécessaire. La seule condition requise est que vous les prépariez avec deux traits d'union. Une fois que vous avez déclaré votre propriété personnalisée, la 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 issu de notre framework CSS d'un jeton de conception en tant que propriété personnalisée et utilisé dans 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 des 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 en tant que valeur dans d'autres propriétés. Nous faisons un usage intensif des 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 possibilité d'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 de donner aux développeurs un contrôle plus précis sur la stylisation de nos composants.

Propriétés personnalisées dans un composant Web Nord

Lorsque nous développons un composant pour notre système de conception, nous adoptons une approche réfléchie de son code CSS : nous cherchons à obtenir un code simple, mais facile à gérer. Les jetons de conception sont définis comme des 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);
  /* ... */
}
Définition des propriétés personnalisées CSS dans 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 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);
  /* ... */
}
Les propriétés personnalisées définies dans la racine fantôme du composant, puis utilisées dans ses styles. Les propriétés personnalisées figurant dans la liste des jetons de conception sont également utilisées.

Nous allons également extraire des 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 qui sont contextuelles par rapport au composant nous offrent deux avantages principaux. Tout d'abord, cela signifie que notre code CSS peut être plus "semi", 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);
}
Propriété personnalisée contextuelle de marge intérieure du groupe d'onglets utilisée à plusieurs endroits du code du composant.

D'autre part, cela permet d'effectuer des changements d'état et de variation très nets des composants : 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 pointage, d'un état actif ou, dans ce cas, d'une variante.


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

Mais 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, dont l'utilisateur peut se servir.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Utiliser le composant de groupe d'onglets sur la page et augmenter la taille de la propriété personnalisée de marge intérieure

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. Grâce à cette approche globale, un composant offre une flexibilité de style suffisante à l'utilisateur tout en contrôlant la plupart des styles réels. En outre, 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 de nos composants de système de conception, mais aussi pour notre équipe de développement lorsqu'elle les utilise dans nos produits.

Aller plus loin avec les propriétés personnalisées

Au moment de la rédaction de ce document, nous ne dévoilons pas réellement ces propriétés personnalisées contextuelles dans notre documentation. Toutefois, nous prévoyons de le faire afin que notre équipe de développement au sens large puisse comprendre et exploiter ces propriétés. Nos composants sont empaquetés sur npm avec un fichier manifeste contenant toutes les informations à leur sujet. Nous utilisons ensuite le fichier manifeste en tant que données lors du déploiement de notre site de documentation, à l'aide d'Eleventy et de sa fonctionnalité Global Data. Nous prévoyons d'inclure ces propriétés personnalisées contextuelles dans ce fichier de données manifeste.

Nous souhaitons également améliorer la manière dont ces propriétés personnalisées contextuelles héritent des valeurs. À l'heure actuelle, par exemple, si vous souhaitez ajuster la couleur de deux composants de séparation, vous devez cibler ces deux composants spécifiquement avec des sélecteurs, ou appliquer la propriété personnalisée directement à l'élément à l'aide de l'attribut de style. Cela peut sembler acceptable, 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éparation 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 que nous devons cibler spécifiquement le séparateur.

Vous devez définir la valeur de la propriété personnalisée directement au niveau du composant, car elle est définie 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 directement, ne sont pas concernés par ce problème et peuvent même être interceptés sur des éléments parents. Comment tirer le meilleur des deux mondes ?

Propriétés personnalisées privées et publiques

Les propriétés personnalisées privées ont été mises en place par Lea Verou. Il s'agit d'une propriété personnalisée contextuelle "privée" sur le composant lui-même, mais définie sur une propriété personnalisée "publique" avec 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);
  /* ... */
}
Le CSS du composant Web de séparation avec les propriétés personnalisées contextuelles ajustées de sorte que le CSS interne s'appuie sur une propriété personnalisée privée, qui a été définie sur une propriété personnalisée publique avec une création 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. Toutefois, le composant héritera harmonieusement 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>
Là encore, il est possible de modifier la couleur du séparateur en ajoutant sa propriété personnalisée contextuelle au sélecteur de section. Le séparateur en héritera, produisant ainsi un extrait de code plus clair et plus flexible.

Bien qu'on puisse affirmer que cette méthode n'est pas vraiment "privée", nous continuons à penser qu'il s'agit d'une solution plutôt élégante à un problème qui nous inquiète. Lorsque nous en saurons l'occasion, nous nous attaquerons à ce problème dans nos composants. Ainsi, notre équipe de développement pourra mieux contrôler leur utilisation tout en bénéficiant des garde-fous que nous avons mis en place.

J'espère que ces informations sur la façon dont nous utilisons les composants Web avec des propriétés CSS personnalisées vous seront utiles. N'hésitez pas à nous donner votre avis. 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 retrouver Nordhealth @NordhealthHQ sur Twitter, ainsi que sur le reste de mon équipe, qui a travaillé dur pour intégrer 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ț