Tu définis une couleur sur un élément depuis trois endroits différents, mais le navigateur choisit celle que tu ne voulais pas. C'est là qu'intervient la CSS specificity : c'est le système de priorité qui décide quelle règle l'emporte quand plusieurs déclarations ciblent le même élément. Une fois que tu comprends la specificity, les heures perdues à se demander « pourquoi ce style ne s'applique pas ? » disparaissent. Au lieu de parsemer ton code de !important au hasard, tu vois exactement pourquoi une règle prend effet ou non.
Qu'est-ce que la cascade et où se place la specificity ?
Le « Cascading » de CSS est le processus par lequel le navigateur ordonne les règles en conflit. Pour désigner un gagnant, il parcourt ces étapes dans l'ordre :
- Origine et importance : d'où vient le style (valeurs par défaut du navigateur, styles utilisateur, styles auteur) et s'il porte
!important. - Specificity : à quel point le sélecteur est « spécifique » — notre sujet principal ici.
- Ordre source : si tout le reste est égal, la règle écrite en dernier dans le document l'emporte.
La specificity ne décide donc pas seule ; c'est un échelon de la cascade. Mais au quotidien, c'est l'échelon qui sème le plus de confusion et provoque le plus de conflits.
Comment se calcule la specificity : une valeur en trois parties
Imagine que chaque sélecteur reçoit une valeur à trois composantes, qu'on écrit souvent comme un triplet (a, b, c) :
- a — sélecteurs d'ID : chaque ID comme
#headervaut un point. - b — classes, attributs et pseudo-classes : chaque
.btn,[type="text"],:hovervaut un point. - c — éléments et pseudo-éléments : chaque
div,a,::beforevaut un point.
La comparaison se fait de gauche à droite : d'abord a, puis b en cas d'égalité, puis c. Quelques exemples :
/* (0,0,1) — un seul élément */
p { color: black; }
/* (0,1,1) — une classe + un élément */
p.intro { color: blue; }
/* (1,0,0) — un seul ID */
#main { color: green; }
/* (1,1,1) — ID + classe + élément */
#main p.intro { color: red; }
Si tous ciblent le même <p class="intro">, le gagnant est #main p.intro avec la valeur la plus haute, donc la couleur est rouge. Un détail crucial : les valeurs ne s'additionnent pas en base dix. Onze classes (0,11,0) ne battront jamais un seul ID (1,0,0). Un seul 1 dans la colonne a surpasse n'importe quel nombre de points dans la colonne b.
Les sélecteurs qui ne comptent pas, et les cas particuliers
Certaines choses ne contribuent en rien à la specificity :
- Le sélecteur universel
*et les combinateurs (>,+,~, l'espace) n'ajoutent rien. :where()produit toujours une specificity nulle, quoi que tu mettes dedans. Parfait pour écrire des valeurs par défaut « souples », faciles à surcharger.:is()et:not()n'ajoutent rien par eux-mêmes mais prennent le score du sélecteur le plus spécifique qu'ils contiennent. Par exemple,:is(#a, .b)compte autant qu'un ID.
/* une règle écrite avec :where() a une specificity nulle */
:where(.card) a { color: teal; }
/* même un simple sélecteur d'élément la bat (0,0,2 > 0,0,1) */
.card a { color: orange; }
!important et styles inline : les briseurs d'ordre
Deux choses échappent au calcul normal de la specificity. La première est le style inline, l'attribut style, plus fort que toute règle basée sur un sélecteur (conceptuellement une quatrième colonne, la plus à gauche). La seconde est !important : l'ajouter à une déclaration la déplace dans une couche distincte et supérieure.
<p id="x" style="color: green">Bonjour</p>
#x { color: red; } /* l'inline gagne → vert */
#x { color: blue !important; } /* !important bat même l'inline → bleu */
Quand deux déclarations !important s'affrontent, la specificity normale et l'ordre source tranchent à nouveau. Le conseil pratique est clair : traite !important comme un dernier recours. Dès que tu commences à l'utiliser, le surcharger exige un autre !important, et ta feuille de styles vire à la course à l'escalade.
Éviter les guerres de specificity en pratique
L'objectif est de travailler avec une specificity faible et cohérente :
- Utilise des classes plutôt que des ID : pour styler, les ID apportent un score inutilement élevé. Garde tes sélecteurs basés sur les classes.
- Garde des sélecteurs peu profonds : au lieu de
.nav ul li a, cible directement une seule classe comme.nav-link. - Utilise les cascade layers : avec
@layer, tu fixes explicitement la priorité des couches ; une couche plus tardive bat la précédente, indépendamment de la specificity. - Utilise
:where()pour des valeurs par défaut souples afin que les utilisateurs d'un composant puissent surcharger les styles facilement.
Ouvre le panneau « Styles » des DevTools de ton navigateur : les déclarations barrées montrent instantanément quelle règle a été surchargée et laquelle a gagné. Lire ce panneau au lieu de deviner est le moyen le plus rapide d'assimiler la specificity.
Questions fréquentes
Que se passe-t-il à specificity égale ?
Si l'origine, l'importance et la specificity sont toutes égales, l'ordre source décide : la règle définie en dernier dans le CSS l'emporte. C'est pourquoi placer une surcharge après la règle existante suffit souvent.
!important gagne-t-il toujours ?
Parmi les styles auteur, il bat presque toujours les déclarations normales. Mais si une autre déclaration !important a une specificity plus haute ou vient plus tard, c'est elle qui gagne. Quand c'est possible, construis une structure qui n'a jamais besoin de !important.
Quelle est la différence entre :is() et :where() ?
Les deux regroupent une liste de sélecteurs et ciblent les mêmes éléments. La différence est la specificity : :is() prend le score du sélecteur le plus spécifique qu'il contient, tandis que :where() produit toujours zéro.
Si tes conflits de styles ont grandi et que ton CSS s'est transformé en une pile ingérable de !important, je peux t'aider à migrer l'architecture vers une structure basée sur les classes et en couches. Pour discuter d'un projet, contacte-moi.