Ce blog est encore en construction
Notes de la conférence The CSS Cascade, a deep dive
- Conférence : CSS Days, cssday.nl
- Speaker : Bramus Van Damme
- Liens de la conférence : Video Youtube (en), slides
- Publié le : 26 Décembre 2024
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.
- l’origine et l’importance
- le contexte
- les styles attachés aux éléments
- la spécificité
- 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.
- les déclarations liées aux transitions CSS
- les déclarations des user-agents avec
!important
- les déclarations de l’utilisateur avec
!important
- les déclarations de l’auteur avec
!important
- les déclarations liées aux animations CSS
- les déclarations de l’auteur
- les déclarations de l’utilisateur
- les déclarations des user-agent
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).
- (0,0,0) pour
*
(spécificité la plus basse regroupant tous les élements) - (0,0,1) est le poids des balises
span, div, p
- (0,1,0) est le poids des classes et sélecteurs
.classe, href[''], :hover
- (1,0,0) est le poids des id
#identifiant
Les sélecteurs peuvent être aussi combinés, ce qui se refléte dans leur spécifité:
#sidebar .title
vaudra (1,1,0)#sidebar > .title
vaudra aussi (1,1,0) car le>
n’affecte pas la spécificité#sidebar ul.secondary li a
vaudra (1,1,3)
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 :
- l’origine et l’importance
- le contexte
- les styles attachés aux éléments
- les layers
- la spécificité
- 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.