Accueil / Articles / Comment la cascade choisit un gagnant : importance, spécificité et ordre des sources

Comment la cascade choisit un gagnant : importance, spécificité et ordre des sources

Voyez comment les navigateurs résolvent les déclarations CSS concurrentes, comment interpréter la spécificité comme une comparaison en quatre étapes, et pourquoi les règles hover et !important vous surprennent si souvent.

2447 mots

Lorsque plusieurs règles CSS ciblent le même élément et définissent la même propriété, le navigateur ne peut pas les appliquer toutes. Il a besoin d’une méthode déterministe pour choisir précisément une seule déclaration, et ce processus explique toute une catégorie de bugs du type « pourquoi mon style est-il ignoré ? ». À la fin de ce guide, vous serez en mesure de calculer la spécificité d’un sélecteur, de prédire quelle déclaration l’emportera, et de diagnostiquer des cas difficiles tels qu’une règle :hover qui ne s’active jamais, sans avoir recours à !important.

Quel est le rôle de cela dans le pipeline d’affichage

À un niveau général, le navigateur transforme le markup et les styles en pixels à travers une série d’étapes. L’HTML est analysé pour former le DOM, le CSS pour former le CSSOM ; les deux sont ensuite combinés en un arbre d’affichage, avant que l’agencement et la peinture ne produisent ce que vous voyez :

HTML
  ↓
DOM
  ↓
CSS
  ↓
CSSOM
  ↓
Render Tree
  ↓
Layout
  ↓
Paint
  ↓
Pixels

Dans les étapes de traitement CSS se cachent trois questions liées. Premièrement, lorsque plusieurs déclarations entrent en concurrence, laquelle l’emporte ? Deuxièmement, une fois un gagnant choisi, quelle est réellement la valeur qui lui est attribuée ? Troisièmement, que se passe-t-il lorsque un élément ne possède aucune valeur pour une propriété ? Les réponses sont la cascade (avec la spécificité comme élément central), le traitement des valeurs et l’héritage. Ces sujets sont souvent enseignés comme étant indépendants, mais dans le navigateur ils représentent des étapes successives du même processus. Ce guide se concentre sur la première question. Si vous souhaitez en savoir plus sur ce qui se passe après le traitement des styles, consultez comment le navigateur affiche l’interface et quel est le rôle de React.

Règles, déclarations et valeurs concurrentes

D’abord, quelques notions de vocabulaire. Une règle CSS se compose d’un selecteur suivi d’un bloc de déclaration :

.button {
  background-color: blue;
}

Dans cette règle, .button est le selecteur, background-color: blue; est une déclaration, background-color est la propriété et blue est la valeur. La valeur telle que vous l’avez écrite s’appelle le valeur déclarée.

Un vrai fichier de style contiendra souvent plusieurs déclarations pour la même propriété sur le même élément. Une règle peut cibler tous les boutons :

button {
  background-color: red;
}

alors que d’autres, peut-être dans un fichier différent, ciblent les boutons en général et un bouton spécifique par son id :

button {
  background-color: blue;
}

#submit {
  background-color: green;
}

Si un seul <button id="submit"> correspond aux trois règles, quelle couleur d’arrière-plan devrait-il avoir ? C’est là que intervient la cascade. Elle résout les conflits en tenant compte de l’importance de chaque déclaration, de la spécificité de son sélecteur et de l’ordre dans lequel les déclarations apparaissent. Une fois l’importance prise en compte, c’est généralement la spécificité qui détermine le résultat.

La cascade complète décrite dans les spécifications prend également en compte l’origine d’un feuille de style (valeurs par défaut du navigateur, styles de l’utilisateur, styles de l’auteur) ainsi, dans le CSS moderne, les niveaux de cascade déclarés avec @layer. Pour les feuilles de style classiques sans niveaux, c’est l’importance, la spécificité et l’ordre d’origine qui constituent les trois critères à considérer.

Qu’est-ce que mesure la spécificité

La spécificité est la mesure, par le navigateur, de la précision avec laquelle un sélecteur cible un élément. Tous les sélecteurs n’ont pas la même importance. Comparez un sélecteur de type :

p {
  color: red;
}

un sélecteur de classe :

.text {
  color: blue;
}

et un sélecteur id :

#title {
  color: green;
}

Si les trois correspondent au même élément, le navigateur les classe selon une hiérarchie fixe des types de sélecteurs, du plus fort au plus faible :

Inline styles
     ↓
IDs
     ↓
Classes / pseudo-classes / attributes
     ↓
Elements / pseudo-elements

Lorsque les déclarations concurrentes ont une importance égale, le sélecteur le plus spécifique l’emporte. Ici, la règle id l’emporterait et le texte serait en vert.

Remarque importante concernant la première ligne : les styles en ligne définis via l’attribut style ne sont pas des sélecteurs, et la spécification actuelle les traite comme une étape distincte qui prend le pas sur toute déclaration d’auteur basée sur des sélecteurs. Les considérer comme la « colonne » de spécificité la plus élevée, comme c’est généralement fait, donne le même résultat en pratique ; c’est pourquoi le reste de ce guide conserve ce modèle pratique.

Interpréter la spécificité en quatre colonnes

Le malentendu le plus courant est que la spécificité correspond à un seul score qui peut être additionné. Il vaut mieux l’entendre comme une tuplique de quatre valeurs, une par catégorie :

Inline   |   IDs   |   Classes   |   Elements

Pour un sélecteur donné, on compte le nombre de parties appartenant à chaque catégorie. Un sélecteur de classe unique constitue le cas le plus simple :

.button {
  background: blue;
}

Il ne contient aucun style en ligne, aucune id, une seule classe et aucun élément :

Inline styles → 0
IDs           → 0
Classes       → 1
Elements      → 0

ce que l’on peut écrire de manière concise comme :

0, 0, 1, 0

Prenons maintenant un sélecteur plus complexe :

nav#main .button div {
  background: green;
}

Il contient un id (#main), une classe (.button) et deux sélecteurs de type (nav et div), donc sa spécificité est :

0, 1, 1, 2

Pour comparer deux sélecteurs, le navigateur lit ces tuples de gauche à droite, en commençant par la colonne la plus significative. La première colonne où ils diffèrent détermine le résultat, et les colonnes situées à sa droite n’ont plus d’importance. C’est pourquoi un seul id l’emporte sur n’importe quel nombre de classes, et une seule classe l’emporte sur n’importe quel nombre de sélecteurs de type : il n’y a pas de transmission des valeurs d’une colonne à l’autre, donc dix classes ne « compensent » jamais un id. Vous n’avez pas besoin de mémoriser des calculs arithmétiques ; il vous suffit de retenir l’ordre de comparaison.

Examen d’un conflit réel

Considérez un bouton disposant à la fois d’une classe et d’un id. Notez que il s’agit de balises HTML simples, ce qui explique l’utilisation de class plutôt que de className en JSX :

<button class="button" id="submit">
  Don't Click
</button>

Supposons maintenant que le fichier de style contienne une règle de classe :

.button {
  background: blue;
}

ainsi que plusieurs autres règles, dont un sélecteur de type, un sélecteur de descendant long et une règle id-plus-classe avec un état d’hover :

button {
  background: purple;
}

nav#main .button div {
  background: green;
}

#submit.button:hover {
  background: yellow;
}

Toutes ces règles déclarent background, ce qui entraîne une concurrence entre elles. Le navigateur ne choisit pas simplement celle qui apparaît en dernier ; il compare d’abord leur spécificité.

Un détail peut facilement être manqué dans ce bloc : nav#main .button div vise en réalité un div imbriqué à l’intérieur d’un élément ayant la classe button, et non le bouton lui-même. Comme l’élément ciblé par un sélecteur est sa partie la plus à droite, cette règle ne correspond jamais à notre <button>, quelle que soit sa spécificité. Vérifier si une règle correspond est toujours l’étape zéro du débogage.

Pour les règles qui correspondent effectivement, comparez un sélecteur de classe :

.button

à un sélecteur de type simple :

button

Le premier concerne une classe, le second un seul élément. Donc :

.button

prévaut sur :

button

et le bouton est bleu, et non violet, même si la règle violette apparaît plus tard.

Ajoutez un id à la combinaison :

#submit.button

L’id place ce sélecteur dans une colonne supérieure par rapport à tout élément construit uniquement à partir de classes et de types. En comparant de gauche à droite, la colonne id décide immédiatement du résultat, c’est pourquoi un seul id peut surpasser un sélecteur composé de nombreuses classes.

Pourquoi une règle :hover correcte peut ne rien faire

Ce cas provoque beaucoup de confusion lors du débogage. Commencez par une règle de base pour le bouton :

#submit.button {
  background: red;
}

et une règle hover pour le même élément :

#submit.button:hover {
  background: yellow;
}

La règle hover contient un id, une classe et une pseudo-classe, ce qui lui confère plus de spécificité que la règle de base ; ainsi, le survol rend le bouton jaune comme prévu.

Imaginons maintenant qu’une autre partie du code applique un style au même bouton à l’aide d’un sélecteur beaucoup plus long :

nav#main div#container #submit.button {
  background: red;
}

alors que la règle hover reste inchangée :

#submit.button:hover {
  background: yellow;
}

La règle hover contient toujours une pseudo-classe :

:hover

Les pseudo-classes sont bien prises en compte dans la colonne des classes. Mais cela ne ajoute qu’un point au niveau de la classe. Le sélecteur long contient trois identifiants contre un seul pour la règle hover, ce qui lui permet de gagner dans la colonne des identifiants avant même que les classes ne soient comparées. Le résultat est une règle :hover syntaxiquement parfaite, correspondant à l’élément, mais ne provoquant aucun changement à l’écran.

La leçon à retenir est que lorsque les états d’interaction semblent défaillants, le pseudo-class est rarement la cause. Le véritable problème réside généralement dans le fait qu’une autre déclaration est plus spécifique. Les outils de développement du navigateur rendent cela visible : le panneau Styles liste toutes les règles correspondantes et barre celles qui ont perdu, ce qui vous indique précisément quel sélecteur l’emporte sur le vôtre.

Les empates sont résolues par l’ordre d’apparition

Parfois, deux sélecteurs ont une spécificité identique. Prenons deux règles utilisant le même sélecteur de classe :

.button {
  background: red;
}
.button {
  background: blue;
}

Ils sont tout aussi spécifiques et tout aussi importants, donc aucun des deux premiers niveaux ne peut trancher. Le navigateur recourt alors à l’ordre de source : la déclaration qui apparaît en dernier l’emporte. Avec cet ordre :

.button {
  background: red;
}
.button {
  background: blue;
}

le bouton devient bleu.

Tout le processus de décision peut être envisagé comme une série de règles de détermination en cas d’égalité :

Importance
    ↓
Specificity
    ↓
Source Order

Chaque niveau n’est consulté que si le précédent n’a pas pu désigner un gagnant. L’importance prime d’abord ; en cas d’égalité, c’est la spécificité qui décide ; si cela est également égal, c’est la dernière déclaration qui l’emporte.

Le coût d’utilisation de !important

Presque tous les développeurs ont utilisé cette solution de secours au moins une fois :

color: red !important;

L’ajout de !important augmente l’importance d’une déclaration. Comme l’importance est vérifiée avant la spécificité, une déclaration marquée de cette manière peut surpasser une autre déclaration ayant une spécificité bien plus élevée. Par exemple :

.button {
  background: purple !important;
}

elle l’emportera sur une déclaration normale lorsqu’il s’agit d’un sélecteur long contenant de nombreux identifiants. Lorsque deux déclarations !important entrent en concurrence, le navigateur revient à comparer leur spécificité, puis leur ordre.

Cette puissance est précisément ce qui la rend risquée. Une spirale de débogage courante se présente ainsi :

"My style isn't working."
          ↓
"Let's increase the specificity."
          ↓
"Still not working."
          ↓
"Let's add !important."
          ↓
    "It works!"

Le style apparaît enfin, mais le conflit sous-jacent n’a pas disparu ; il a été transféré à la personne suivante qui doit surcharger cette propriété et qui doit alors faire face à un !important de son propre côté. À mesure que de tels conflits s’accumulent, il devient de plus en plus difficile de comprendre le feuille de style. Considérez !important comme une solution de dernier recours, et interprétez un besoin soudain de l’utiliser comme un signe que le CSS nécessite probablement un refactoring.

Au lieu de commencer à écrire :

!important

posez une question plus utile : pourquoi ma déclaration n’a-t-elle pas le dessus ? Ensuite, vérifiez les niveaux dans l’ordre suivant :

  • Une déclaration concurrente est-elle plus importante ?
  • Un sélecteur concurrent est-il plus spécifique ?
  • Une déclaration concurrente apparaît-elle plus tard dans le code source ?

Des utilisations légitimes existent bel et bien, comme les classes utilitaires conçues pour s’appliquer systématiquement ou pour surcharger les styles en ligne injectés par un widget tiers que l’on ne peut pas modifier, mais elles doivent être intentionnelles et non réflexives.

Rédiger des sélecteurs qui fonctionnent naturellement

Il y a ici un aspect plus large lié à la maintenabilité. Lorsqu’un style ne s’applique pas, il est tentant de rendre le sélecteur de plus en plus long jusqu’à ce qu’il fonctionne :

body div section nav ul li a.button {
  color: red;
}

Cela marche, mais chaque élément supplémentaire augmente les exigences pour toute future surcharge, lie le style à une structure DOM spécifique et rend la feuille de style plus difficile à lire. Plutôt que de se demander comment faire en sorte qu’un sélecteur fonctionne à tout prix, il convient de se demander comment organiser le CSS afin que la déclaration prévue l’emporte d’elle-même. Limiter la plupart des sélecteurs à une seule classe, éviter d’utiliser des identifiants pour le stylage, et regrouper les surcharges plus spécifiques près des règles qu’elles modifient sont tous des moyens utiles.

Cela est particulièrement important dans de grands bases de code où de nombreuses personnes écrivent du CSS. La spécificité vise à rendre les résultats prévisibles, et non à déclencher une course aux armements entre sélecteurs.

Utilisation de l’ordre des sources avec des feuilles de style tierces

L’ordre des sources devient un outil pratique lorsque vous combinez vos propres styles avec un fichier de réinitialisation ou une feuille de style tierce. Ces fichiers définissent généralement des styles pour des éléments courants, et vous souhaitez que vos règles les surprennent. Charger votre feuille de style après les leurs rend cela simple :

<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">

Lorsque vos sélecteurs ont la même spécificité que ceux de la bibliothèque, le fichier chargé en dernier l’emporte ; donc placer style.css après reset.css permet à vos déclarations de prendre effet sans augmenter inutilement la spécificité des sélecteurs.

Compter sur l’ordre a aussi un coût. Si quelqu’un réordonne plus tard les balises <link> ou les imports dans un point d’entrée du bundler, les styles peuvent changer silencieusement. Lorsque c’est possible, privilégiez des règles dont la spécificité rend claire la valeur préférée, et utilisez l’ordre des sources comme dernier critère de décision, tel que conçu. Les couches en cascade, là où vos navigateurs cibles les prennent en charge, constituent un moyen plus explicite d’indiquer « la bibliothèque vient en premier, nos styles ensuite ».

Après le gagnant : la valeur en cascade

À ce stade, la première question est résolue. Le navigateur a collecté toutes les déclarations concernant la propriété, évalué d’abord l’importance, puis la spécificité, ensuite l’ordre des sources, pour en choisir une. Cette valeur gagnante est appelée la valeur en cascade.

Cependant, le travail n’est pas encore terminé. Supposons que la déclaration gagnante soit :

width: 66%;

Un pourcentage de ce type ne peut pas être affiché directement ; le navigateur doit encore le traiter, en le comparant au bloc contenant pour obtenir une longueur réelle. Ce traitement des valeurs, associé à l’héritage pour les propriétés qui n’ont aucune déclaration, constitue l’étape suivante pour transformer le CSS en pixels.

Points clés

  • La cascade résout les conflits dans un ordre fixe : importance, puis spécificité, enfin ordre de source.
  • La spécificité est une comparaison en quatre colonnes (en ligne, identifiants, classes/pseudo-classes/attributs, éléments/pseudo-éléments) lue de gauche à droite ; les colonnes inférieures n’ont jamais d’influence sur les colonnes supérieures.
  • Vérifiez que une règle correspond bien à l’élément avant de comparer sa spécificité ; la partie la plus à droite d’un sélecteur indique l’élément ciblé.
  • Une règle :hover ou une autre pseudo-classe ne ajoute qu’un poids au niveau de la classe et peut être éclipsée par une règle de base plus spécifique.
  • !important l’emporte en modifiant le niveau d’importance, et non en résolvant le conflit ; utilisez-le de manière intentionnelle et modérée.
  • Préférez des sélecteurs courts basés sur des classes ainsi qu’un ordre logique des feuilles de style afin que la déclaration souhaitée l’emporte sans escalade.
  • Lectures complémentaires