Vous écrivez .sidebar .card { color: blue; }, vous chargez la page, et la carte reste rouge à cause d’une règle ajoutée une heure plus tôt : .card { color: red; }. Votre nouvelle règle arrive plus tard dans le fichier. Elle perd quand même. Le navigateur compare les deux sélecteurs avec un système de points fixe, et .sidebar .card obtient un score plus élevé que .card, peu importe lequel a été écrit en dernier.
Ce système de points, c’est la spécificité. Une fois que vous savez la calculer d’un coup d’œil, “pourquoi mon CSS ne s’applique pas” devient de l’arithmétique. Cela n’a rien à voir avec la mise en page : Grid ou Flexbox décide où se place un élément, la spécificité décide quelle règle fixe sa couleur ou sa taille quand deux règles entrent en conflit.
Comment le navigateur note un sélecteur
Chaque sélecteur a un poids composé de trois nombres, généralement écrits comme un triplet séparé par des tirets : ID-classe-type. Additionnez ce que contient le sélecteur, colonne par colonne :
| Colonne | Ce qui compte | Poids par occurrence |
|---|---|---|
| ID | #header | 1-0-0 |
| Classe / attribut / pseudo-classe | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Type / pseudo-élément | div, h1, ::before | 0-0-1 |
Le sélecteur universel * et les combinateurs (l’espace, >, ~, +) ajoutent 0-0-0. Ils décident quels éléments correspondent, pas avec quelle force la règle l’emporte.
Quelques sélecteurs réels :
| |
Pour comparer deux valeurs, on les lit de gauche à droite et on s’arrête à la première colonne qui diffère. 1-1-0 l’emporte sur 0-2-0 parce que la colonne ID (1 contre 0) décide avant même qu’on regarde les classes. Ainsi, #main .card l’emporte sur .sidebar .card .title .text, quelle que soit la longueur du second sélecteur. Un ID surpasse n’importe quel nombre de classes.
Pourquoi une règle CSS plus récente peut tout de même perdre
Revenons à l’exemple du début, avec les nombres calculés :
| |
0-2-0 l’emporte sur 0-1-0 dans la colonne des classes, donc la carte est bleue quel que soit l’ordre dans le fichier. L’ordre dans le fichier est le critère que presque tout le monde invoque en premier, mais il ne s’applique que lorsque les triplets sont identiques :
| |
Les deux règles obtiennent 0-1-0, donc celle qui vient plus tard dans la feuille de style l’emporte (ou dans le bloc <style> suivant, ou dans la feuille liée après l’autre). C’est le seul cas où « la dernière l’emporte » est le bon modèle mental. Cela fonctionne sur les petits projets, c’est pour ça que tout le monde l’apprend en premier, et ça cesse de fonctionner dès qu’un projet dépasse une poignée de sélecteurs.
Les styles inline se situent au-dessus de tout cela. Un attribut style="color: green" sur l’élément l’emporte sur toute règle de toute feuille de style, quel que soit le nombre d’ID qu’elle enchaîne. Seules les déclarations !important peuvent le battre.
| |
| |
!important ne fait pas partie du triplet de spécificité. C’est un niveau séparé, de priorité plus élevée : toute déclaration !important l’emporte sur toute déclaration normale, styles inline compris, indépendamment du poids du sélecteur. Quand deux déclarations !important s’affrontent, la spécificité décide (puis l’ordre dans le fichier), exactement comme sans le drapeau. C’est ainsi qu’un seul !important se transforme en course aux armements. Le développeur suivant qui doit le surcharger n’a plus aucun autre coup disponible qu’un nouveau !important, et bientôt plus personne ne sait quelle règle est censée l’emporter.
Quels sélecteurs n’ajoutent aucune spécificité : :where() contre :is()
Quelques sélecteurs permettent de cibler avec précision sans payer le prix habituel.
:where() apporte toujours 0-0-0, quel que soit ce que vous mettez dedans :
| |
Cela fait de :where() l’enrobage idéal pour les valeurs par défaut et les styles de thème qui doivent rester faciles à surcharger. Une bibliothèque de composants peut livrer ses valeurs par défaut dans :where(), et une simple règle .card { } écrite par celui qui l’utilise les bat sans avoir besoin de !important.
:is(), :not() et :has() fonctionnent différemment. Ils n’ajoutent pas de poids propre, mais reprennent le poids de leur argument le plus spécifique :
| |
Donc :where() est gratuit, et :is() coûte autant que le plus lourd de ce que vous mettez dedans, même si cet argument ne correspond jamais à l’élément.
Comment les cascade layers surclassent la spécificité
Si vous accumulez classes et ID pour gagner des combats que le sélecteur n’aurait jamais dû avoir à livrer, le projet a besoin de @layer, pas de plus d’imbrication.
| |
L’ordre des layers vient de l’endroit où vous déclarez les noms, pas du poids des sélecteurs. utilities a été déclarée en dernier, donc une règle à l’intérieur l’emporte sur une règle dans components, même si .text-blue a le même poids 0-1-0 que .card. La spécificité continue de régler les conflits à l’intérieur d’une layer. Entre layers, l’ordre des layers l’emporte sans appel et la spécificité n’est jamais consultée.
Une règle piège presque tout le monde la première fois : pour les déclarations normales (sans !important), les styles hors de toute layer l’emportent sur les styles à l’intérieur d’une layer. Déplacez votre CSS de composants dans @layer components mais laissez une seule surcharge sans layer, et cette règle non layerisée l’emporte quel que soit l’ordre dans lequel vous avez déclaré les layers. Pour les déclarations !important, la priorité s’inverse : les règles important de la layer déclarée en premier l’emportent sur celles des layers suivantes, et les styles important non layerisés se classent en dernier de tous. L’inversion est volontaire, et c’est la partie de la spécification à relire avant d’en dépendre.
Le support des navigateurs a cessé d’être un argument contre les layers depuis un moment : @layer est disponible dans tous les moteurs majeurs depuis 2022. Le vrai coût est organisationnel. L’équipe doit se mettre d’accord sur un ordre de layers à l’avance (reset, base, components, utilities est un schéma courant) et classer le nouveau CSS dans la bonne layer. C’est plus un problème de processus que de syntaxe.
Erreurs courantes avec la spécificité CSS
- Combattre la spécificité avec plus de spécificité. Chaque surcharge ajoute une classe ou un ID de plus que la précédente, jusqu’à ce que changer une couleur nécessite
#app .page .section .card.is-active.is-selected. C’est le même mode de défaillance que le spaghetti code : chaque correctif ajoute de l’enchevêtrement au lieu de le retirer. La solution n’est presque jamais un sélecteur plus lourd. C’est un sélecteur plus plat, ou une layer. - Dégainer
!importantau premier conflit. Cela gagne le combat immédiat et prépare le suivant, car la prochaine surcharge n’a plus aucun coup de spécificité normale disponible. - Oublier que les pseudo-classes comptent.
:hover,:focus,:nth-child()et l’argument de:not()ajoutent tous0-1-0. Une simplea { color: blue; }plus bas dans le fichier ne surchargera pasa:not(.disabled) { color: red; }écrite au-dessus, parce que la version avec:not()est plus lourde. - Supposer que la valeur héritée gagne parce que la règle “semble” plus forte. Une règle qui cible directement un élément bat toujours une valeur héritée, quelle que soit la spécificité de la règle dont provient l’héritage. Si un enfant affiche la mauvaise couleur, cherchez une règle qui cible cet enfant avant de suspecter l’héritage.
- Glisser
:is()dans un sélecteur en s’attendant à ce qu’il reste léger.:is()prend le poids de son argument le plus lourd, donc:is(.card, #sidebar)est en réalité un sélecteur1-0-0déguisé.
Comment vérifier la spécificité dans les DevTools
Pas besoin de compter les colonnes à la main quand un bug est sous vos yeux. Inspectez l’élément dans Chrome ou Firefox DevTools et ouvrez le panneau Styles (Rules dans Firefox). Chaque règle correspondante est listée dans l’ordre de la cascade, avec les déclarations surchargées barrées. Survolez un sélecteur et les deux navigateurs affichent sa spécificité. À partir de là, vous savez en quelques secondes si vous êtes face à un écart de spécificité, un problème d’ordre de layers, ou un simple ordre dans le fichier entre deux règles de poids égal.
La prochaine fois qu’une règle perd sans raison apparente, calculez les deux triplets avant d’ajouter une classe ou un !important pour la forcer. En général, le bon correctif consiste à retirer du poids au sélecteur qui gagne, pas à en empiler davantage sur celui qui aurait dû gagner.