9 : Les variables CSS

Les concepts

Les propriétés personnalisées CSS (custom properties en anglais, aussi appelés variables CSS) sont des entités définies par les développeurs, contenant des valeurs spécifiques utilisables à travers le document. Elles sont initialisées avec des propriétés personnalisées (par exemple --main-color: black;, ou « main-color » est choisi de façon arbitraire) et accessibles en utilisant la notation spécifique var() (par exemple : color: var(--main-color);).

Des sites et applications web complexes peuvent avoir des feuilles de style où de nombreuses valeurs sont répétées. Ainsi, la même couleur pourra être utilisée à des centaines d’endroits où il faudra la mettre à jour si besoin. Les propriétés personnalisées permettent de stocker une valeur à un endroit puis de réutiliser cette valeur (on factorise ainsi le code).

Utilisation simple

Voici comment on déclare une variable :

element {
  --main-bg-color: brown;
}

Et voici comment on l’utilise

element {
  background-color: var(--main-bg-color);
}

Problématique

Lors de l’élaboration de sites de grande envergure, leurs auteurs font parfois face à des soucis de maintenabilité. De grandes feuilles de styles sont utilisées et de nombreuses informations se répètent. Par exemple, maintenir un thème de couleurs à travers un document nécessite la réutilisation des valeurs des couleurs à plusieurs endroits dans les fichiers CSS. Modifier un thème, en changeant une couleur ou en le récrivant entièrement, devient alors une tâche complexe demandant de la précision, là où un simple trouver et remplacer ne suffit pas.

Le problème peut s’aggraver en utilisant les frameworks CSS puisque modifier une couleur demande de modifier le framework lui-même.

Le deuxième avantage de ces variables vient du fait que le nom lui-même peut contenir des informations sémantiques. Les fichiers CSS deviennent alors plus facile à lire et à comprendre : écrire main-text-color permet de mieux s’y retrouver au fur et à mesure de la lecture qu’une valeur hexadécimale comme #00ff00, surtout si la même couleur est utilisée dans un autre contexte.

Définition

Les propriétés personnalisées ont actuellement deux formes :

  • les variables, qui sont des associations entre un identifiant et une valeur, utilisables à la place de n’importe quelle valeur normale, en utilisant la notation fonctionnelle var() : var(--example-variable) retourne la valeur de --example-variable.
  • les propriétés personnalisées, qui sont des propriétés spéciales notées --* où * représente le nom de la variable. Elles sont utilisées pour définir la valeur d’une variable donnée : --example-variable: 20px; est une déclaration en CSS, utilisant la propriété personnalisée --* pour initialiser la valeur de la variable CSS --example-variable à 20px.

Les propriétés personnalisées sont similaires aux propriétés ordinaires. Elles sont sujettes à la cascade et héritent leur valeur de leur parent si elles ne sont pas redéfinies.

Premiers pas avec les propriétés personnalisées CSS

Commençons avec cette feuille CSS simple colorant les éléments de différentes classes avec la même couleur :

.un {
  color: white;
  background-color: brown;
  margin: 10px;
  width: 50px;
  height: 50px;
  display: inline-block;
}
.deux {
  color: white;
  background-color: black;
  margin: 10px;
  width: 150px;
  height: 70px;
  display: inline-block;
}
.trois {
  color: white;
  background-color: brown;
  margin: 10px;
  width: 75px;
}
.quatre {
  color: white;
  background-color: brown;
  margin: 10px;
  width: 100px;
}
.cinq {
  background-color: brown;
}

Appliquons-le à ce code HTML :

<div>
    <div class="un">Toto</div>
    <div class="deux">Texte <span class="cinq">- encore du texte</span></div>
    <input class="trois">
    <textarea class="quatre">Lorem Ipsum</textarea>
</div>

Ce qui donne ceci :

Remarquez la répétition dans le CSS. La couleur d’arrière-plan est définie à brown à plusieurs endroits. Certaines déclarations peuvent être faites plus haut dans la cascade et le problème se résoudra grâce à l’héritage. Mais pour des projets non-triviaux, cela n’est pas toujours possible. En déclarant une variable dans la pseudo-classe :root, un développeur CSS peut éviter certaines répétitions en utilisant cette variable.

:root {
  --main-bg-color: brown;
}
.un {
  color: white;
  background-color: var(--main-bg-color);
  margin: 10px;
  width: 50px;
  height: 50px;
  display: inline-block;
}
.deux {
  color: white;
  background-color: black;
  margin: 10px;
  width: 150px;
  height: 70px;
  display: inline-block;
}
.trois {
  color: white;
  background-color: var(--main-bg-color);
  margin: 10px;
  width: 75px;
}
.quatre {
  color: white;
  background-color: var(--main-bg-color);
  margin: 10px;
  width: 100px;
}
.cinq {
  background-color: var(--main-bg-color);
}

Ce code donne le même résultat que précédemment mais permet de n’utiliser la propriété désirée qu’une seule fois.

Héritage des propriétés personnalisées et valeurs par défaut

Il y a un héritage des propriétés personnalisées. Cela signifie que si une propriété n’est pas définie sur un élément, la valeur prise en compte sera celle utilisée pour la propriété de l’élément parent.
Soit le fragment de code suivant :

<div class="un">
  <div class="deux">
    <div class="trois">
    </div>
    <div class="quatre">
    </div>
  </div>
</div>

associé à cette feuille de style :

.deux {
  --test: 10px;
}
.trois {
  --test: 2em;
}

Dans ce cas, les résultats de var(--test) seront :

  • 10px pour l’élément avec class="deux"
  • 2em pour l’élément avec class="trois"
  • 10px pour l’élément avec class="quatre" : la valeur est héritée depuis le parent
  • invalid value pour l’élément avec class="un", c’est la valeur par défaut utilisée pour les différentes propriétés personnalisées.

Avec var() on peut définir plusieurs valeurs par défaut lorsque la variable donnée n’est pas définie. Cela peut s’avérer utile lorsqu’on travaille avec des éléments personnalisés (Custom Elements) et le Shadow DOM.

Le premier argument passé à la fonction est le nom de la propriété personnalisée qui doit être substituée. Le deuxième argument, s’il est fourni, indique la valeur par défaut qui est utilisée lorsque la propriété personnalisée en question est invalide.

.deux {
  color: var(--my-var, red); 
  /* Red si --my-var n'est pas définie */
}
.trois {
  background-color: var(--my-var, var(--my-background, pink)); 
  /* rose (pink) si --my-var et --my-background ne sont pas définies */
}
// Suite invalide :
.trois {
  background-color: var(--my-var, --my-background, pink); 
}

Note : Des problèmes de performances ont pu être observés causant un rendu plus lent des pages car le navigateur doit analyser l’ensemble des variables pour voir si elles sont disponibles.

Validité et valeurs

Le concept classique de validité en CSS, lié à chaque propriété, n’est pas très utile en ce qui concerne les propriétés personnalisées. Quand la valeur d’une propriété personnalisée est lue, le navigateur ne sait pas à quel moment elle sera utilisée. Il doit donc considérer quasiment toutes les valeurs comme valides.

Malheureusement, ces valeurs valides peuvent être utilisées, via la notation fonctionnelle var(), dans un contexte où cela n’aurait pas de sens. Les propriétés et variables personnalisées peuvent mener à des déclarations CSS invalides, conduisant à un nouveau concept de valide lors de l’exécution.

Gestion des variables invalides

Lorsque le navigateur analyse une substitution var() invalide, c’est la valeur initiale ou héritée de la propriété qui est utilisée. Par exemple :

<p>La couleur initiale d'un paragraphe est noire.</p> 

lié ausx styles :

:root { --text-color: 16px; } 
p { color: blue; } 
p { color: var(--text-color); }

Comme on pourrait s’y attendre, le valeur applique la substitution aec --text-color à la place de var(--text-color) mais 16px n’est pas une valeur valide pour color. Après la substitution, la déclaration n’a plus aucun sens. Le navigateur résoud ce problème en deux étapes :

  1. Il vérifie si la propriété peut être héritée (ici color) : c’est bien le cas mais dans notre exemple <p> n’a aucun parent avec une couleur définie, il passe donc à l’étape suivante.
  2. La valeur utilisée est la valeur initiale par défaut, pour color, c’est black.

Manipulation des variables en JavaScript

Il est possible d’utiliser les valeurs des propriétés personnalisés en JavaScript de la même façon que les propriétés standards.

// obtenir une variable à partir d'un style en ligne (dans un élément html)
element.style.getPropertyValue("--ma-variable");

// obtenir une variable par ailleurs
getComputedStyle(element).getPropertyValue("--ma-variable");

// définir une variable dans un style en ligne
element.style.setProperty("--ma-variable", varJS + 4);

Cas d’usage des propriétés personnalisées

Fonction de couleurs

Les propriétés personnalisées ne représentent pas seulement des valeurs de propriété entières, elles peuvent également être utilisées pour stocker des valeurs partielles. Un cas d’utilisation fréquemment cité concerne les fonctions de couleur CSS. HSLA se prête particulièrement bien aux propriétés personnalisées, nous permettant en tant que développeurs un niveau de contrôle sans précédent lorsqu’il s’agit de mélanger les couleurs.

.some-element {
  background-color: hsla(
    var(--h, 120),
    var(--s, 50),
    var(--l, 50),
    var(--a, 1)
  );
}
.some-element.darker {
  --l: 20;
}

Propriétés abrégées

Si vous utilisez une propriété abrégée telle qu’une animation et que vous devez modifier une valeur pour un élément différent, la réécriture de la propriété entière peut être source d’erreurs et ajoute une charge de maintenance supplémentaire. En utilisant des propriétés personnalisées, nous pouvons ajuster très facilement une seule valeur dans la propriété raccourcie :

.some-element {
  animation: var(--animationName, pulse) var(--duration 2000ms) ease-in-out
    infinite;
}
.some-element.faster {
  --duration: 500ms;
}
.some-element.shaking {
  --animationName: shake;
}

Valeurs répétées

Supposons que nous ayons un élément qui a une valeur cohérente pour son rembourrage supérieur, mais la même valeur pour tous les autres côtés. Écrire ce qui suit pourrait être un peu fastidieux, surtout si nous voulons ajuster les valeurs de remplissage :

.some-element {
  padding: 150px 20px 20px 20px;
}
@media (min-width: 50em) {
  padding: 150px 60px 60px 60px;
}

L’utilisation de propriétés personnalisées signifie que nous n’avons qu’un seul endroit pour ajuster ce rembourrage. Mieux encore, s’il s’agit d’une valeur standard utilisée sur l’ensemble du site, nous pourrions la déclarer dans un fichier de configuration dédié.

:root {
  --pad: 20px;
}
@media (min-width: 50em) {
  :root {
    --pad: 60px;
  }
}
.some-element {
  padding: 150px var(--pad) var(--pad) var(--pad);
}

Calculs complexes

Les propriétés personnalisées peuvent être très pratiques pour stocker des valeurs calculées (à partir de la fonction calc ()), qui peuvent elles-mêmes être calculées à partir d’autres propriétés personnalisées.

On peut utiliser des propriétés personnalisées avec clip-path si on a besoin de calculer un chemin par rapport à un autre ou par rapport à des variables connues. Le code suivant calcule les points de trajectoire du clip pour deux pseudo-éléments pour donner l’apparence d’un élément « sectionné ».

.element {
  --top: 20%;
  --bottom: 80%;
  --gap: 1rem;
  --offset: calc(var(--gap) / 2);
}
.element::before {
  clip-path: polygon(
    calc(var(--top) + var(--offset)) 0,
    100% 0,
    100% 100%,
    calc(var(--bottom) + var(--offset)) 100%
  );
}
.element::after {
  clip-path: polygon(
    calc(var(--top) - var(--offset)) 0,
    calc(var(--bottom) - var(--offset)) 100%,
    0 100%,
    0 0
  );
}

Animations échelonnées

Si nous voulons échelonner les animations pour un certain nombre d’éléments enfants, nous pouvons élégamment définir le retard d’animation sur chacun d’eux en définissant simplement la propriété personnalisée comme index de l’élément :

.element {
  --delay: calc(var(--i, 0) * 500ms);
  animation: fadeIn 1000ms var(--delay, 0ms);
}
.element:nth-child(2) {
  --i: 2;
}
.element:nth-child(3) {
  --i: 3;
}

Malheureusement, nous devons actuellement assigner la variable explicitement, ce qui pourrait être un problème si nous avons un nombre indéterminé d’enfants. Le recours à du JS qui pourrait s’occuper de cela en affectant l’index de l’élément en tant que variable, pourrait se révéler utile pour ce type d’animation échelonnée. Mais ce serait bien de ne pas avoir à utiliser JS !

Adam Argyle a récemment soumis une proposition pour deux nouvelles fonctions CSS, sibling-count() et sibling-index(), qui changeraient la donne, rendant beaucoup de nouvelles choses possibles avec CSS. Ils ne sont pas du tout adoptés par les navigateurs à ce stade, mais ce serait un ajout puissant, donc à surveiller.

Système de grille flexible

Commençons par un système strandard de grille sur 12 colonnes en utilisant flex :

.container {
  max-width: 960px;
  margin: 0 auto;
  display: flex;
}
.col-1 { flex-basis: 8.333%; }
.col-2 { flex-basis: 16.666%; }
.col-3 { flex-basis: 25%; }
.col-4 { flex-basis: 33.333%; }
.col-5 { flex-basis: 41.666%; }
.col-6 { flex-basis: 50%; }
/* ainsi de suite jusqu'à 12... */

Il y a beaucoup de répétitions et de valeurs codées en dur ici. Sans oublier combien d’autres seront générés une fois que nous ajouterons plus de points d’arrêt pour le responsive design, etc.

Création d’un système de grille avec des propriétés personnalisées CSS

Commençons par le HTML pour arriver au résultat suivant :

Il se compose de trois éléments: un en-tête, une section de contenu et une barre latérale. Créons un balisage pour cette vue, en donnant à chacun des éléments une classe sémantique unique (en-tête, contenu, barre latérale) et une classe de colonne qui indique que cet élément fait partie d’un système de grille :

<div class="container">
  <header class="header column">
    header
  </header>
  <main class="content column">
    content
  </main>
  <aside class="sidebar column">
    sidebar
  </aside>
</div>

Notre système de grille, comme précédemment, est basé sur une disposition à 12 colonnes. Vous pouvez l’imaginer comme une superposition couvrant nos zones de contenu :

Donc .header prend les 12 colonnes, .content prend huit colonnes (66.6% de la largeur totale) et .sidebar prend quatre colonnes (33.3% de la largeur totale). Dans notre CSS, nous aimerions pouvoir contrôler la largeur de chaque section en modifiant une seule propriété personnalisée :

.header {
  --width: 12;
}
.content {
  --width: 8;
}
.sidebar {
  --width: 4;
}

Pour le faire fonctionner, il suffit d’écrire une règle pour la classe .column.

.container {
  display: flex;
  flex-wrap: wrap;
  margin: 0 auto;
  max-width: 960px;
}
.column {
  --columns: 12; /* Number of columns in the grid system */
  --width: 0; /* Default width of the element */

  flex-basis: calc(var(--width) / var(--columns) * 100%);
}

Notez deux choses ici :

  1. La variable --columns est désormais déclarée à l’intérieur de la règle .column. La raison en est que cette variable n’est pas censée être utilisée en dehors de la portée de cette classe.
  2. L’équation mathématique que nous effectuons dans la propriété flex-based est maintenant incluse dans une fonction calc().

À un niveau très basique, c’est tout !
Nous venons de créer un système de grille à 12 colonnes avec des propriétés personnalisées CSS.
Mais … nous avons généralement besoin d’un système de grille un peu plus sophistiqué. Et c’est là que les choses deviennent vraiment intéressantes.

Ajout d’un point d’arrêt à la grille

La plupart du temps, nous avons besoin de mises en page différentes pour différentes tailles d’écran. Supposons que dans notre cas, nous souhaitons que la mise en page reste telle qu’elle est sur une grande fenêtre (par exemple, le bureau), mais que les trois éléments deviennent pleine largeur sur des écrans plus petits (par exemple, mobiles).

Donc, dans ce cas, nous aimerions que nos variables se présentent comme suit :

.header {
  --width-mobile: 12;
}
.content {
  --width-mobile: 12;
  --width-tablet: 8; /* Tablet and larger */
}
.sidebar {
  --width-mobile: 12;
  --width-tablet: 4; /* Tablet and larger */
}

.content et .sidebar contiennent chacun deux variables maintenant. La première variable (--width-mobile) est le nombre de colonnes qu’un élément doit prendre par défaut, et la seconde (--width-tablet) est le nombre de colonnes qu’un élément doit prendre sur des écrans plus grands. L’élément .header ne change pas; il prend toujours toute la largeur. Sur les écrans plus grands, l’en-tête doit simplement hériter de la largeur qu’il a sur mobile.

Maintenant, mettons à jour notre classe .column.
Pour que la version mobile fonctionne comme prévu, nous devons modifier la classe .column comme suit :

.column {
  --columns: 12; /* Number of columns in the grid system */
  --width: var(--width-mobile, 0); /* Default width of the element */
  
  flex-basis: calc(var(--width) / var(--columns) * 100%);
}

Fondamentalement, nous remplaçons la valeur de la variable --width par --width-mobile. Notez que la fonction var() prend maintenant deux arguments. Le premier d’entre eux est une valeur par défaut. Il dit: « Si une variable --width-mobile existe ici, affectez sa valeur à la variable --width. » Le deuxième argument est un repli. En d’autres termes: « Si une variable --width-mobile n’est pas déclarée ici, affectez cette valeur de secours à la variable --width. » Nous avons défini cette solution de rechange pour préparer un scénario dans lequel certains éléments de la grille n’auront pas une largeur spécifiée.

Par exemple, notre élément .header a une variable --width-mobile déclarée, ce qui signifie que la variable --width lui sera égale et que la propriété flex-based de cet élément sera calculée à 100% :

.header {
  --width-mobile: 12;
}

.column {
  --columns: 12;
  --width: var(--width-mobile, 0); /* 12, takes the value of --width-mobile */
  
  flex-basis: calc(var(--width) / var(--columns) * 100%); /* 12 ÷ 12 × 100% = 100% */
}

Si nous supprimons la variable --width-mobile de la règle .header, la variable --width utilisera une valeur de secours :

.header {
  /* Rien ici... */
}

.column {
  --columns: 12;
  --width: var(--width-mobile, 0); /* 0, takes the the fallback value */
  
  flex-basis: calc(var(--width) / var(--columns) * 100%); /* 0 ÷ 12 × 100% = 0% */
}

Maintenant que nous avons compris comment définir un repli pour les propriétés personnalisées CSS, nous pouvons créer un point d’arrêt, en ajoutant une requête média à notre code :

.column {
  --columns: 12; /* Number of columns in the grid system */
  --width: var(--width-mobile, 0); /* Default width of the element */
  
  flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
  .column {
    --width: var(--width-tablet); /* Width of the element on tablet and up */
  }
}

Cela fonctionne exactement comme prévu, mais uniquement pour le contenu et la barre latérale, c’est-à-dire pour les éléments qui ont spécifié à la fois --width-mobile et --width-tablet. Pourquoi ?

La requête média que nous avons créée s’applique à tous les éléments .column, même ceux qui n’ont pas de variable --width-tablet déclarée dans leur portée. Que se passe-t-il si nous utilisons une variable qui n’est pas déclarée? La référence à la variable non déclarée dans une fonction var() est alors considérée comme invalide au moment de la valeur calculée, c’est-à-dire invalide au moment où un agent utilisateur essaie de la calculer dans le contexte d’une déclaration donnée.

Idéalement, dans un tel cas, nous aimerions que la déclaration --width: var(--width-tablet); soit ignorée et que la déclaration précédente de --width: var(--width-mobile, 0); soit utilisée à la place. Mais ce n’est pas ainsi que fonctionnent les propriétés personnalisées. En fait, la variable --width-tablet non valide sera toujours utilisée dans la déclaration de flex-basis. Une propriété qui contient une fonction var() non valide calcule toujours sa valeur initiale. Donc, comme flex-basis: calc(var(--width) / var(--columns) * 100%); contient une fonction var() invalide, la propriété entière appliquera auto (la valeur initiale pour flex-based).

Que pouvons-nous faire d’autre alors? Définir un repli ! Comme nous l’avons appris précédemment, une fonction var() contenant une référence à la variable non déclarée, calcule sa valeur de secours, tant qu’elle est spécifiée. Donc, dans ce cas, nous pouvons simplement définir un repli sur la variable --width-tablet :

.column {
  --columns: 12; /* Number of columns in the grid system */
  --width: var(--width-mobile, 0); /* Default width of the element */
  
  flex-basis: calc(var(--width) / var(--columns) * 100%);
}

@media (min-width: 576px) {
  .column {
    --width: var(--width-tablet, var(--width-mobile, 0));
  }
}

Cela créera une chaîne de valeurs de repli, faisant que la propriété --width utilisera --width-tablet lorsqu’elle sera disponible, puis --width-mobile si --width-tablet n’est pas déclaré, et finalement, 0 si aucune des variables n’est déclaré. Cette approche nous permet d’effectuer de nombreuses combinaisons :

.section-1 {
  /* Flexible on all resolutions */
}

.section-2 {
  /* Full-width on mobile, half of the container's width on tablet and up */
  --width-mobile: 12;
  --width-tablet: 6;
}
  
.section-3 {
  /* Full-width on all resolutions */
  --width-mobile: 12;
}
  
.section-4 {
  /* Flexible on mobile, 25% of the container's width on tablet and up */
  --width-tablet: 3;
}

Il semble que nous ayons fait beaucoup de travail pour accomplir une tâche relativement simple. Pourquoi ? La réponse principale est : pour rendre le reste de notre code plus simple et plus facile à gérer. En fait, nous pourrions construire la même disposition en utilisant les techniques décrites dans la partie précédente de cet article :

.container {
  display: flex;
  flex-wrap: wrap;
  margin: 0 auto;
  max-width: 960px;
}

.column {
  --columns: 12; /* Number of columns in the grid system */
  --width: 0; /* Default width of the element */

  flex-basis: calc(var(--width) / var(--columns) * 100%);
}

.header {
  --width: 12;
}

.content {
  --width: 12;
}

.sidebar {
  --width: 12;
}

@media (min-width: 576px) {
  .content {
    --width: 6;
  }
  
  .sidebar {
    --width: 6;
  }
}

@media (min-width: 768px) {
  .content {
    --width: 8;
  }
  
  .sidebar {
    --width: 4;
  }
}

Dans un petit projet, cette approche pourrait parfaitement fonctionner. Pour les solutions plus complexes, je suggérerais d’envisager une solution plus évolutive.

Pourquoi devrais-je quand même l’envisager?

L’utilisation de propriétés personnalisées présente quelques avantages :

  1. C’est du CSS. En d’autres termes, il s’agit d’une solution plus standardisée et fiable, indépendante de tout tiers. Aucune compilation, aucune version de package, aucun problème étrange. Cela fonctionne simplement (à l’exception des navigateurs où cela ne fonctionne tout simplement pas).
  2. C’est plus facile à déboguer. Vous pouvez afficher et déboguer le code directement dans un navigateur, tout en travaillant avec des variables CSS, tout le code est disponible (et en direct!) Directement dans DevTools.
  3. C’est plus facile à maintenir. Les propriétés personnalisées nous permettent de rendre nos variables plus contextuelles et, par conséquent, plus maintenables. De plus, ils sont sélectionnables par JavaScript.
  4. C’est plus flexible. Notez que le système de grille que nous avons construit est extrêmement flexible. Si vous souhaitez utiliser une grille à 12 colonnes sur une page et une grille à 15 colonnes sur une autre, c’est très simple, comme il s’agit d’une seule variable. Le même code peut être utilisé sur les deux pages.
  5. Cela prend moins de place. Même si le poids des fichiers CSS n’est généralement pas le principal goulot d’étranglement des performances de chargement des pages, il va sans dire que nous devons viser à optimiser les fichiers CSS lorsque cela est possible. Pour donner une meilleure image de ce qui peut être économisé, une petite expérience a été faite, en prenant le système de grille de Bootstrap et en le reconstruisant à partir de zéro avec des propriétés personnalisées. Les résultats sont les suivants : la configuration de base de la grille Bootstrap génère plus de 54 Ko de CSS tandis qu’une grille similaire faite avec des propriétés personnalisées ne fait que 3 Ko. C’est une différence de 94%! De plus, l’ajout de colonnes à la grille Bootstrap rend le fichier encore plus volumineux. Avec les variables CSS, nous pouvons utiliser autant de colonnes que nous voulons sans affecter la taille du fichier.