Blogf

Ce blog est encore en construction

Notes de la conférence The CSS Cascade, a deep dive

Quelques mots sur la conférence et le talk

NDLR: ces notes ont rédigées 2 ans après la conférence mais le sujet est toujours d’actualité mais certains points (notamment concernant la conférence et/ou le conférencier) peuvent ne plus être d’actualité.

CSS Days est une conférence se déroulant à Amsterdam chaque année depuis 2013 et produisant une quinzaine de talks internationaux sur des sujets divers liés aux fonctionnalités avancées (et parfois très poussées) du CSS et du HTML. Les speakers et les sujets sont sélectionnés par les organisateurs (pas de CFP) et les videos sont toutes présentes sur Youtube.

Bramus Van Damme est un passionné de HTML, CSS et Javascript (dans cet ordre). Au moment de la conférence il était Chrome Developer Relation Engineer chez Google (NDLR: comme beaucoup de speakers cette année-là), dans l’équipe CSS UI and DevTools.

Dans ce talk, il revient sur la notion de cascade, le fameux C de CSS, et notamment sur la spécificité. Il y introduit aussi les CSS layers, une fonctionnalité encore trop peu utilisée.

La Cascade

Avant d’introduire les cascade layers, le “vrai” sujet de cette conférence, Bramus commence par repréciser la notion de cascade en CSS.

Prenons ce simple morceau de code HTML:

<input type="text" id="name" style="color: blue;" />

et quelques déclarations CSS associées:

input { color: grey; }
input[type="text"] { color: hotpink; !important; }
#name { color: lime; }

Comment le navigateur sait-il quelle couleur de texte appliquer ? Première, deuxième, troisième ? C’est là qu’intervient la notion de cascade. Sa définition officielle est un peu alambiquée mais Bramus Van Damme en donne une définition plus simple:

The CSS Cascade is the algorithm that determines the winner from a group of competing declarations

en français: la cascade CSS est l’algorithme qui détermine la vainqueur parmi une liste de déclarations en compétition

Les étapes de la cascade

Le navigateur va comparer les déclarations selon les règles suivantes, dans l’ordre, et à chaque conflit va passer à l’étape suivante pour déterminer la vainqueur. Chacune des étapes va être présentée par la suite.

  1. l’origine et l’importance
  2. le contexte
  3. les styles attachés aux éléments
  4. la spécificité
  5. l’ordre d’apparence

1. L’origine et l’importance

L’origine d’une déclaration est définie par rapport à l’endroit d’où elle provient, et l’importance concerne l’utilisation du mot-clé !important. L’utilisation de ce dernier va venir inverser certains origines.

Il existe 8 sortes d’origines pour les déclarations, listées ci-dessous de la plus haute à la plus basse priorité:

Notes: Les déclaration des user-agent sont les styles par défaut des navigateurs, celles de l’auteur (le développeur) sont celles du site et celles de l’utilisateur sont celles qu’un utilisateur peut avoir personnalisées dans son navigateur.

D’après cette étape, c’est la déclaration suivante dans l’exemple du début qui l’emporte grâce à l’usage de !important:

input[type="password"] { color: hotpink; !important; }

2. Le contexte

La règle concernant le contexte est que la déclaration qui provient du contexte extérieur est celle qui gagne. Lorsque l’on ajoute un !important, la règle devient la déclaration qui provient du contexte intérieur est celle qui gagne.

3. Les styles reliés aux éléments

Il s’agit des déclarations de ce style, et elles prennent le pas sur les déclarations similaires effectués via une règle de style (en CSS par exemple.) :

<input type="password" style="color: blue;" />

La déclaration ci-dessus prendra le pas sur celle-ci :

input[type="password"] { color: hotpink; }

La spécificité

La règle concernant la spécificité est simple : la déclaration avec la plus grande spécificité l’emporte, mais c’est aussi une des plus complexes.

Petit rappel sur la spécificité (parfois aussi appelée poids). Il s’agit d’une triade (A,B,C), ayant comme valeurs par défaut (0,0,0).

Les sélecteurs peuvent être aussi combinés, ce qui se refléte dans leur spécifité:

Le calcul entre deux poids s’effectue en comparant d’abord les valeurs A, puis B, puis C. Et non pas en les comparant à des valeurs entières (113, 100, 1).

Reprenons notre exemple initial (sans la notion d’!important pour l’instant)

input { color: grey; } /* (0,0,1) */
input[type="password"] { color: hotpink; } /* (0,1,1) */
#password { color: lime; } /* (1,0,0) */

Cela semble simple, mais évidemment en CSS il existe quelques cas particuliers:

:is()

Le selecteur :is() (lien MDN) est une pseudo-classe CSS prenant des sélecteurs CSS en argument et ciblant les éléments concernés par ces sélecteurs. C’est une manière d’écrire plus concise, mais le calcul de la spécificité est différente que pour les sélecteurs dissociés. En effet, c’est la spécificité de plus haut niveau parmi les arguments qui est prise.

ul > li#highlighted, /* (1,0,0) */
ul > li.active {    /* (0,1,0) */
    ... 
}

/* s'écrit aussi */

ul > li:is(#highlighted, .active) { ... }
/* dans le cas du selecteur :is c'est la spécificité de plus haut niveau qui est prise: (1,0,0) la spécificité du #highlighted */
:where()

Le selecteur :where() (lien MDN) est très similaire à :is() à la différence qu’il a toujours une spécificité de (0,0,0).

ul > li:where(#highlighted, .active) { ... }
/* le bloc where(#highlighted, .active) vaut (0,0,0)
le calcul porte donc sur le bloc ul > li et le résultat est (0,0,2) */
:nth-child()

Ces selecteurs lien MDN permettent de cibler les enfants d’un élément à partir d’un critère de positionnement (le 2ème, les enfants pairs etc.).

/* syntaxe classique */
:nth-child(n+2) { ... }

/* syntaxe avec of <selector> */
li:nth-child(2n + 1  of div, .highlight) { ... }
/* les spécificités valent: div (0,0,1), .highlight (0,1,0), li (0,0,1)
néanmoins il faut aussi compter la spécifité de nth-child (0,1,0)
au final c'est la spécificité du sélecteur li:nth-child(2n + 1 of .highlight) donc le plus élevé des sous-selecteurs qui indiquera la spécifité finale: (0,2,1) */

Tips: il existe des calculateurs de spécificité en ligne, par exemple celui de Polypane CSS Selector Specificity sur le site de Polypane. Celui-ci a l’avantage d’expliquer le résultat.

L’ordre d’apparition

Si la spécificité n’a pas suffit à départager des déclarations c’est enfin l’ordre d’apparition dans le document qui prévaut et c’est le dernier apparu dans le document qui l’emporte.

Un exemple pour le principe :

.password input { color: grey; }
.password input { color: lime; } /* le vainqueur */

Introduction aux Cascade Layers

La plupart des soucis rencontrés dans la cascade de nos déclarations se situent généralement dans les étapes liées à la spécificité et l’ordre d’apparence (les deux dernières). Notamment parce que les premières étapes sont généralement les mêmes.

Bramus présente quelques exemples de code qui peuvent poser soucis. Le premier est le cas d’un reset CSS, c’est à dire l’ajout de code CSS destiné à fournir une base de CSS déchargée des surcharges navigateurs. On se retrouve souvent en conflit avec des règles de ces reset dont la spécificité est trop haute (c’est le but) pour pouvoir les surcharger simplement. Il faut alors jouer avec les sélecteurs de notre déclaration pour en augmenter la spécificité, au risque de changer la règle de ce qu’on souhaite appliquer.

Le second exemple est celui du recours aux surcharges des déclarations via l’utilisation d’un !important pour en augmenter le poids car les déclarations CSS de plus hauts poids, que nous maitrisons ou non, ne sont pas facilement surchargeables. Cela marche grâce à la règle de la 1ère étape de la cascade (l’origine) mais ça ressemble le plus souvent à du bidouillage.

Le dernier exemple est celui de classes utilitaires utilisées pour aligner du texte (.u-text-left, .u-text-center, .u-text-right) et une autre classe faisant un travail similaire (.card__content) par exemple elle aligne le texte au centre. Que se passe-t’il si on applique sur une div deux classes ayant des règles contradictoires <div class="u-text-right card__content"> ? Et bien il faut garder en tête que c’est la dernière règle appliquée qui gagnera.

Souvent, on pourrait se dire qu’il suffit de réordonner le code pour mettre les déclarations importantes en dernier mais ça n’est pas toujours possible. Notamment parce que nous n’avons pas toujours la main sur ce genre de détail.

Et si nous n’avions pas à nous soucier de ça ? C’est là qu’intervient une nouvelle étape dans la cascade: les Layers

Les layers

NDLR: le termes layer n’est pas traduit au fil du texte mais il se traduit en français par couche.

Reprenons nos étapes initiales et venons y ajouter cette nouvelle étape :

  1. l’origine et l’importance
  2. le contexte
  3. les styles attachés aux éléments
  4. les layers
  5. la spécificité
  6. l’ordre d’apparence

La définition des Cascade Layers est la suivante :

With Cascade Layers you get to slice up single-origin styles in several layers, and control the priority of each layer.

en français: avec les Cascades Layers vous pouvez découper les styles provenant d’une même origine en plusieurs couches, et contrôler la priorité de chacune de ces couches.

Et cela s’utilise via la directive @layer. Que nous allons illustrer en introduisant 2 layers autonomes et nous allons donner à chacun une priorité.

@layer reset {
    ul[class] {
        margin: 0;
    }
}

@layer components {
    .nav {
        margin: 0 40px;
    }
}

La priorité est donnée par défaut dans l’ordre d’apparition inversée (le dernier est prioritaire).

NDLR: à l’écriture de ce compte-rendu fin 2024, la règle @layer est Baseline c’est à dire qu’elle a été implémentée par tous les principaux navigateurs. Lien MDN

Rentrons un peu plus en profondeur dans les couches

La réutilisation des noms de layers

Tout comme n’importe quelle déclaration CSS il est tout à fait possible de réutiliser un nom de layer. En fait, il ne s’agit pas réellement d’une ré-utilisation: les deux layers vont être fusionnés en un seul.

Les layers anonymes

Un layer n’a pas besoin d’être nommé, il peut fonctionner sans :

@layer { ... }

Par contre les layers anonymes ne sont pas fusionnés, ils sont considérés distinctement.

Prédéfinir des ordres de priorité

Comment pouvons-nous revoir la priorité de nos layers sans constamment les changer de place dans nos fichiers de styles ? Pour ça on peut aussi utiliser la règle @layer pour indiquer un ordre de priorité pour une liste de layers, de cette façon:

/* Généralement on va placer cette ligne en haut du fichier */
@layer reset, base, components, utilities;

@layer reset {...};
@layer base {...};
@layer utilities {...};
@layer components {...};

Utilisation avec des fichiers CSS externes

Il est aussi possible d’attribuer des layers à des fichiers CSS importés:

@layer reset, base, components, utilities;

@import url(reset.css) layer(reset);
@import url(base.css) layer(base);
@import url(utilities.css) layer(utilities);
@import url(components.css) layer(components);

@layer components {...};

Utilisation de styles avec et sans layers

Que se passe-t-il quand on utilise à la fois des styles avec et sans layers ? Ce sont les styles sans layers qui prendront la plus haute priorité.

Imbrication de layers

Oui, il est possible d’imbriquer des layers.

@layer theme {
    @layer light {
        :root {
            --primary-color: #000;
        }
    }
    @layer light {
        :root {
            --primary-color: #fff;
        }
    }
};

/* s'utilise avec une notation de ce type */

@layer theme.dark {
    :root {
        --primary-color: #000;
    }
}

Utilisation avec !important

Que se passe-t-il quand on utilise un !important ? La règle est la même que précédemment, les propriétés utilisant cette règle seront prioritaires. A noter que cela sépare l’origine des propriétés. Des propriétés avec ou sans !important dans un même layer ne sont plus considérés comme ayant la même origine.

Revert une propriété

La notion de revert (en français: revenir en arrière) n’est pas propre aux propriétés utilisées dans des layers, (lien MDN). Dans le cas de layers, elle applique la règle du layer précédent.

@layer base {
    a {
        color: lime;
    }
}

@layer theme {
    a {
        color: hotpink;
    }
    a.normal {
        color: revert-layer; /* vaudra: lime */
    }
}

Utilisation pratique des Cascade Layers

Quelques tips du présentateur: utiliser la définition des priorités via la propriété @layer en début de fichier, et donner aux styles externes leur propre layer. Ces styles qui semblaient durs à manager en début de conférence, n’auront plus d’interference avec vos propres déclarations.

@layer reset, thirdparty; 

@import url(reset.css) layer(reset); 
@import url(carousel.css) layer(thirdparty); 
@import url(map.css) layer(thirdparty ); 

/* ensuite votre propre style */

Résumé

La Cascade CSS comporte désormais 6 étapes, la nouvelle (les Layers) se positionnant juste avant les étapes de spécificité et d’ordre d’apparence.

Ils sont très utiles pour ajouter une nouvelle zone de séparation dans les déclarations CSS.