Comprendre la spécificité CSS

Ou enfin comprendre pourquoi ce langage n'en fait qu'à sa tête


Sommaire

  1. Introduction
  2. Le problème observable
  3. Les règles du jeu
  4. Le vrai problème : la maintenabilité
  5. Une première partie de la solution : le specificity graph
  6. L'autre partie de la solution : simplifier nos sélecteurs
  7. Conclusion : de la méthode et des bonnes pratiques

1 - Introduction

La spécificité fait partie des deux notions les plus importantes du langage CSS, l'autre étant évidemment la fameuse cascade.

La compréhension de cette spécificité est essentielle à une bonne maîtrise du langage, et fait à l'heure actuelle défaut à trop de développeurs pourtant expérimentés. La raison : cette notion n'est que trop rarement abordée dans les cours/tutos/livres/formations sur CSS. Quand elle l'est, c'est souvent de façon beaucoup trop concise, imprécise voire inexacte.

Évidemment il existe sur le web quantité de ressources directement consacrées à la spécificité. Il est possible de trouver quantité de tutos, articles, vidéos et packages dédiés à cela. Mais ce sont généralement des ressources qui sont spécifiquement dédiées à la spécificité. Cela représente un apprentissage supplémentaire à celui du langage étant donné que la notion est trop souvent absente des cours pour débutants ou des livres traitant de l'ensemble du langage. Et pourtant cette notion est si importante qu'elle devrait faire partie de tous les programmes d'apprentissage de CSS. C'est le problème que j'essaye de contribuer à solutionner en incluant cette notion dans mes cours, en insistant dessus.

C'est aussi ce que je tente de faire ici en improvisant ce tuto.

2 - Le problème observable

Lorsqu'on apprend CSS, on apprend en général qu'en cas de conflits entre deux déclarations de propriétés, c'est celle écrite dans la règle la plus bas dans le code CSS qui "gagne".

Pour illustrer, considérons le code HTML suivant :

<header class="header-big"></header>
Puis le CSS suivant :
header {
    font-size: 20px;
}

.header-big {
    font-size: 24px;
}

Nous avons ici deux règles de CSS différentes qui vont toutes les deux s'appliquer à notre balise <header>.

La première règle a pour sélecteur header soit le nom de la balise. Ce sélecteur signifie exactement "toutes les balises

de la page". Il s'appliquera donc par défaut chaque fois que cette balise sera utilisée.

La deuxième règle a pour sélecteur .header-big soit le nom d'une classe associée à la balise. Ce sélecteur signifie exactement "toutes les balises qui ont la classe header-big". Il s'appliquera donc par défaut sur toutes les balises possédant cette classe.

Ces deux règles s'appliquent ici à la même balise HTML, et déclarent toutes les deux la propriété font-size. On est donc face à un conflit. Comme dit plus haut : c'est la règle écrite la plus bas qui gagne.

Donc le texte aura ici une taille de 24px, telle que définie dans la deuxième règle.

Jusqu'ici tout va bien.

Le problème survient lorsque l'on inverse l'ordre des deux règles :

.header-big {
    font-size: 24px;
}

header {
    font-size: 20px;
}

Ici on s'attend à ce que, comme précédemment, la règle écrite la plus bas soit prioritaire. Et donc à ce que le texte ait une taille de 20px. Mais non.

Le texte fait toujours 24px.

Nous avons inversé l'ordre des deux règles et pourtant c'est toujours la règle avec le sélecteur .header-big qui l'emporte.

Qu\'est-ce donc cette diablerie
C'est la fameuse spécificité qui provoque ce comportement.

3 - Les règles du jeu

La spécificité CSS, c'est la spécificité de vos sélecteurs. C'est, si vous préférez, leur complexité. Plus un sélecteur est considéré comme spécifique (donc complexe) plus il sera prioritaire sur les autres en cas de conflit.

On peut donc observer via notre exemple précédent que le sélecteur .header-big est considéré comme plus spécifique que le sélecteur header. Et donc en déduire qu'en CSS les classes sont plus spécifiques que les noms de balises.

Il existe un outil en ligne très pratique pour calculer la spécificité d'un sélecteur CSS, le Specificity Calculator

Si on essaye d'entrer nos deux sélecteurs dedans, voici ce qu'on obtient :

CSS Specificity Calculator - Résultat n°1
Les valeurs de spécificité renvoyées par le calculator pour nos deux sélecteurs

Le calculator nous renvoie des valeurs numériques correspondant à la spécificité de chacun des deux sélecteurs. Pour bien lire ces valeurs, il faut les lire un petit peu comme on lit des numéros de versions d'application :

  • le sélecteur .header-big a une spécificité qui vaut 0.1.0
  • le sélecteur header a une spécificité qui vaut 0.0.1

Et on compare ces valeurs comme on comparerait des numéros de versions d'application, c'est à dire nombre par nombre : d'abord les deux premiers nombres entre eux (0 et 0), puis les deux seconds (1 et 0), puis les deux derniers (0 et 1).

  • On compare donc d'abord 0 et 0 : égalité
  • Puis on compare 1 et 0 : le premier sélecteur gagne
  • Pas besoin de comparer les derniers nombres, le premier sélecteur a déjà gagné

Bon ok pour comparer deux valeurs entre elles, mais pour le calcul on va devoir utiliser ton lien à chaque fois ?

Non. Uniquement lorsque vous aurez des sélecteurs trop complexes pour réussir à calculer la spécificité de tête. Dans un exemple comme le notre, il aurait été très facile de calculer cela nous-mêmes, sans passer par le site calculator. Il suffit de compter les occurrences des différentes parties de notre sélecteur : le nombre de balises, le nombre de classes, le nombre d'identifiants, etc. Cela fonctionne comme ceci :

  • Le premier nombre de la valeur de spécificité correspond au nombre d'identifiants. Ainsi le sélecteur #intro #title + #subtitle aura une valeur de spécificité de 3.0.0 ;
  • Le deuxième nombre de la valeur de spécificité correspond au nombre de classes ou pseudo-classes. Ainsi le sélecteur .special:hover aura une valeur de spécificité de 0.2.0 ;
  • Le troisième et dernier nombre de la valeur de spécificité correspond au nombre d'éléments (noms de balises) ou pseudo-éléments. Ainsi le sélecteur article p::before aura une valeur de spécificité de 0.0.3 ;

Imaginons deux sélecteurs beaucoup plus complexes :

CSS Specificity Calculator - Résultat n°2
Ca se complique un peu...
  • Le premier possède 1 identifiant : #homepage ;
  • Le premier possède 2 classes : .viewport-mobile et .active ;
  • Le premier possède 1 pseudo-classe : :hover ;
  • Le premier possède 2 éléments : body et section ;

=> Le premier sélecteur a donc une valeur de spécificité égale à 1 . ( 2 + 1 ) . 2 = 1.3.2

  • Le second possède 0 identifiant ;
  • Le second possède 3 classes : .menu-navbar, .menu-link et .disabled ;
  • Le second possède 2 éléments : a et a ;
  • Le second possède 1 pseudo-élément : ::first-letter ;
  • La fonction :not() n'étant ni un identifiant, ni une classe, ni une pseudo-classe, ni un élément, ni un pseudo-élément, elle est ignorée dans le calcul de spécificité (elle compte pour 0 si vous préférez) ;

=> Le second sélecteur a donc une valeur de spécificité égale à 0 . 3 . ( 2 + 1 ) = 0.3.3

Et donc si l'on souhaite les comparer, c'est le premier qui gagne. On compare d'abord le nombre d'identifiants : le premier sélecteur en a 1 et le second en a 0. Pas besoin de comparer la suite, le premier sélecteur a gagné.

Ok, donc il faut savoir que les sélecteurs les plus spécifiques sont toujours prioritaires. Et qu'on peut calculer facilement la valeur de spécificité pour chaque sélecteur, soit de tête soit avec le site Specificity Calculator. C'est tout ?

Non. Tout cela crée un problème supplémentaire : la difficulté de maintenir un code CSS sur le long terme.

4 - Le vrai problème : la maintenabilité

Jusqu'ici nous avons comparé deux règles entre elles. Facile d'oberver laquelle est prioritaire et d'adapter son code en conséquence. Maintenant imaginez dans un code CSS de plusieurs milliers de lignes, comprenant des centaines de sélecteurs dont certains très complexes (et donc très spécifiques). Imaginez que ce fichier soit celui d'un site en production, et qu'une équipe de plusieurs développeurs front maintienne ce fichier, chaque développeur de l'équipe étant susceptible d'intervenir dessus. C'est à dire de modifier ou rajouter du CSS parmi ces milliers de lignes déjà existantes.

Il en résulte une grande difficulté à savoir où écrire son code pour qu'il fonctionne. Le réflexe de départ veut que toute nouvelle règle ajoutée vienne se placer à la suite du code existant, plus bas dans le fichier. Mais vous l'avez vu avec notre exemple plus haut : si une règle avec un sélecteur plus spécifique précède, alors la notre perdra les conflits éventuels.

Cette situation est loin d'être rare. Tout projet un peu sérieux comprend très vite un code CSS assez conséquent, qui a tendance à s'allonger sur la durée. Cette frustration d'écrire un code CSS qui ne fonctionne pas du tout, TOUS les développeurs web front-end la connaissent. Elle est à l'origine de beaucoup de mépris envers le langage lui-même, de la part de gens n'ayant jamais compris/appris/entendu parler de la notion de spécificité. Et qui sont pourtant parfois d'excellents développeurs.

Face à ce genre de cas, souvent mêlés à la nécessité de respecter les délais, on se retrouve la plupart du temps à appliquer l'une des deux méthodes suivantes, relevant davantage du "patch à l'arrache" que de la réelle solution :

  • On peut vouloir volontairement compliquer le sélecteur de notre nouvelle règle, pour être certain qu'il soit prioritaire. Rajouter plusieurs identifiants, classes, éléments, etc.
  • Ou on peut utiliser !important sur toutes les propriétés qui ne s'appliquent pas, pour forcer leur priorité.

Dans un cas comme dans l'autre, ces solutions sont très mauvaises sur le long terme. Elles condamnent les développeurs du projet à les utiliser de plus en plus fréquemment. Jusqu'à ce qu'ils finissent avec un fichier bourré de sélecteurs beaucoup trop complexes inutilement et/ou de !important partout pour chaque propriété ou presque. Petit à petit, semaine après semaine, itération après itération, le fichier CSS lui-même se transforme en une dette technique longue de plusieurs milliers de lignes, de plus en plus impossible à réparer.

Voilà exactement ce qui fait que CSS a la réputation d'être très difficile à maintenir sur le long terme. Ce qui fait que durant le cycle de vie d'un projet, on se retrouvera plusieurs fois à raser entièrement notre code CSS pour le recréer de zéro faute de pouvoir encore le faire évoluer.

A titre d'exemple, le code CSS de Bootstrap utilise près de 1300 fois le mot-clé !important. C'est BEAUCOUP BEAUCOUP BEAUCOUP trop. Même dans un projet si conséquent que Bootstrap, !important ne devrait pas apparaître plus d'une dizaine de fois. Idéalement, il ne devrait pas du tout apparaître. Sur le long terme, le code de Bootstrap est condamné à être entièrement réécrit ou à devenir impossible à faire évoluer. Et pourtant Bootstrap n'est pas plus "mal fait" qu'un autre. Le projet est sérieux, les développeurs qui y contribuent sont tout à fait compétents. Le problème n'est pas Bootstrap. Bulma CSS l'utilise 504 fois. Foundation CSS l'utilise 59 fois, ce qui est déjà nettement mieux mais toujours trop. L'écrasante majorité des codes CSS du monde, y compris sur de très très gros sites web, fait face à la même situation.

Parce que la notion de spécificité est trop souvent ignorée. Parce que les développeurs web de ma génération ne l'ont pas apprise lorsqu'ils débutaient, et ne l'ont découverte que trop tard (quand ils l'ont découverte).

S'ils l'avaient apprise et comprise, ils auraient pu en déduire depuis bien longtemps les solutions et bonnes pratiques qui suivent.

Ne faites pas comme nous à l'époque : n'ignorez pas cette notion. N'ignorez pas les bonnes pratiques qui suivent. Elles feront de vous quelqu'un avec une bien meilleure maîtrise de CSS que la majorité des "devs seniors" actuels.

Que la Spécificité CSS soit avec vous
Que la Spécificité CSS soit avec vous

5 - Une première partie de la solution : le specificity graph

Le specificity graph est une notion amenée par Harry Roberts (https://csswizardry.com/). Notion qui m'aura personnellement permis de comprendre l'importance de la spécificité, notamment via cette vidéo :

Harry Roberts - Managing CSS Projects with ITCSS

Le nom "ITCSS" qu'il donne à sa méthode n'est pas important ici. Pas plus que les catégories qu'il utilise dans son triangle inversé. C'est sa classification, vous pouvez en créer une qui vous soit propre.

Ce qui est important en revanche c'est bien la notion de specificity graph.

L'idée c'est de faire une représentation en graphique de la spécificité de chacun de vos sélecteurs, rapportée à leur position au sein de votre code CSS.

Sur les screens ci-dessous (issus du site d'Harry Roberts), on place en ordonnées le niveau de spécificité de nos sélecteurs, et en abscisses leur position dans le code. Plus c'est haut plus c'est spécifique, plus c'est à droite plus c'est écrit "en bas" du code CSS.

CSS Specificity Graph - Courbe n°1
Un specificity graph en dents de scie est mauvais signe

Ici nous avons l'exemple d'un mauvais specificity graph. Tout est désordonné : vous avez d'abord des règles peu spécifiques, puis des un peu plus spécifiques, puis des très peu spécifiques, puis des très très spécifiques... Et ça continue en dents de scie comme ça. C'est exactement le genre de code CSS que je décrivais plus haut, qui va nous forcer à utiliser des !important partout.

C'est exactement le genre de code CSS qui devrait être réordonné dans l'ordre de spécificité.

A titre de comparaison, voici un bon specificity graph, celui d'un fichier bien fait, dans lequel les règles sont écrites dans l'ordre de spécificité de leurs sélecteurs :

CSS Specificity Graph - Courbe n°2
Voilà une belle courbe 😊

Dans un fichier comme celui-ci, il n'y a PAS BESOIN d'utiliser !important. Sauf besoins trop spécifiques pour les détailler ici, vous pouvez vous en sortir sans écrire !important une seule fois. Et ce même si votre fichier CSS est très long.

Dans un fichier comme celui-ci, tout code CSS qui devra être ajouté durant le cycle de vie du projet pourra être ajouté au bon endroit, en respectant l'ordre de spécificité. Ce qui nous garantit que tout code CSS ajouté au bon endroit va fonctionner du premier coup.

=> La première leçon que l'on peut tirer de tout ceci, la première bonne pratique, c'est de toujours ordonner vos règles CSS selon l'ordre de spécificité de leur sélecteurs.

=> Par extension, une seconde bonne pratique peut être d'inscrire la valeur de spécificité dans un commentaire au-dessus de chaque sélecteur.

En réécrivant notre code de départ selon ces deux principes, ça nous donnerait ceci :

/* Specificity : 0.0.1 */
header {
    font-size: 20px;
}

/* Specificity : 0.1.0 */
.header-big {
    font-size: 24px;
}

On obtient ainsi un code beaucoup plus logique : ce qui est en bas est toujours prioritaire sur ce qui est en haut. La présence des commentaires aide à vérifier cela d'un simple coup d'oeil, nous permettant de savoir très facilement où ajouter du code.

Par exemple, imaginons que l'on veuille rajouter une règle avec pour sélecteur article header. On peut déjà calculer que, puisque ce sélecteur contient 2 éléments, sa valeur de spécificité est de 0.0.2. Ajouter cette règle à notre code au bon endroit donnerait donc ceci :

/* Specificity : 0.0.1 */
header {
    font-size: 20px;
}

/* Specificity : 0.0.2 */
article header {
    font-size: 24px;
}

/* Specificity : 0.1.0 */
.header-big {
    font-size: 24px;
}

Nous avons su faire évoluer notre code facilement et rapidement, en gérant nos conflits dans un ordre logique, tout en conservant un specificity graph qui soit propre.

6 - L'autre partie de la solution : simplifier nos sélecteurs

Afin de nous simplifier la tâche d'avoir à calculer des valeurs de spécificité et d'ordonner nos règles, quelques bonnes pratiques supplémentaires peuvent aider. Elles visent presque toutes le même objectif : simplifier nos sélecteurs au maximum.

En premier les identifiants. Ils font la même chose que les classes, sauf qu'on est obligés de les utiliser une seule fois côté HTML. Les classes n'ont pas cet inconvénient. Selon tout ce que l'on vient de découvrir ensemble, ils ont également un autre inconvénient : ils sont plus spécifiques que les classes, les noms de balises, les pseudo-classes et les pseudo-éléments. Ils sont toujours prioritaires sur tout ça. Ce qui nous force à les écrire à la fin de notre code CSS, afin qu'ils soient à l'extrême en abscisses et en ordonnées sur la courbe de notre specificity graph (tout en haut à droite). Cette méthode règle certes les problèmes éventuels, respecte les bonnes pratiques énoncées précédemment... Mais on peut se simplifier la vie encore plus. Il suffit de ne plus du tout utiliser d'identifiant en CSS. On peut toujours s'en servir en HTML (notamment pour créer des ancres ou relier des balises de formulaires à l'atribut for des labels). Mais on arrête complètement de les utiliser côté CSS. On peut s'en passer car tout ce qui est faisable avec est faisable avec les classes. Ne plus du tout utiliser d'identifiant nous permet d'avoir un specificity graph moins complexe, des différences moins grandes entre nos différents sélecteurs. Et donc un code plus simple à ordonner. Et donc à maintenir sur le long terme.

Ensuite, les sélecteurs du DOM : le   (l'espace), le >, le ~ et le +. Dans la majorité des cas, ces sélecteurs peuvent être simplifiés en les remplaçant par la création d'une nouvelle classe. Ce qui impose évidemment de modifier le HTML pour attribuer cette nouvelle classe aux balises concernées. Cela représente un petit effort entièrement justifié par la compensation : là encore on va simplifier grandement notre specificity graph, là encore on va avoir moins de différences entre nos sélecteurs. On "réduit les inégalités" entre nos règles CSS. Et réduire les inégalités ça fait toujours du bien. Là aussi notre code sera plus simple à ordonner, et donc à maintenir sur le long terme.

Ensuite, les styles inline. C'est à dire les styles écrits côté HTML, directement sur les balises à l'aide de l'attribut style. Par exemple : <header style="font-size:16px"></header>. Ces déclarations sont prioritaires sur tout (sauf !important) : les identifiants, les classes et pseudo-classes, les éléments et pseudo-éléments. Pour des raisons évidentes on va préférer éviter au maximum le recours à ces styles inline. C'est de toute façon une façon très limitée d'écrire du CSS, donc on ne perd pas grand chose à s'en servir le moins possible.

Enfin le fameux !important. Vous l'avez maintenant compris il est prioritaire sur TOUT le reste : les styles inline, les identifiants, les classes et pseudo-classes, les éléments et pseudo-éléments. C'est le joker par excellence. Mais c'est aussi très dangereux d'avoir recours à cette méthode, pour les raisons expliquées plus haut. C'est mettre un pied dans l'engrenage consistant à l'utiliser de plus en plus souvent, jusqu'à ne plus pouvoir s'en passer. La règle à retenir est donc de ne JAMAIS utiliser !important.

Dernier détail, les règles groupées avec , : elles comptent bien comme plusieurs règles de CSS, avec chacune son sélecteur et donc sa spécificité. Par exemple, on aurait pu réécrire nos règles précédentes comme ceci :

/* Specificity : 0.0.1 */
header {
    font-size: 20px;
}

/* Specificity : 0.0.2, 0.1.0 */
article header, .header-big {
    font-size: 24px;
}

La dernière règle est une règle groupée, c'est en fait deux règles en une. On a donc bien deux valeurs de spécificité différentes ici, qui pourront toutes les deux être utilisées par le navigateur pour effectuer des calculs de priorité selon les conflits rencontrés. C'est à dire que même si l'on a groupé deux de nos règles, le sélecteur header peut toujours rentrer en conflit avec article header et/ou avec .header-big.

Pour ordonner correctement vos règles, vous placerez donc celles qui sont groupées selon la valeur de spécificité la plus forte parmi tous les sélecteurs qu'elles contiennent. Dans notre exemple, pour savoir où placer notre règle groupée, on tient compte du 0.1.0 et non du 0.0.2.

7 - Conclusion : de la méthode et des bonnes pratiques

En résumé, la spécificité CSS est une valeur calculable, qui est utilisée par le navigateur pour déterminer la priorité des déclarations en cas de conflit.

Les règles de priorité fonctionnent comme ceci :

  • Toute propriété possédant !important sera prioritaire sur tout le reste ;
  • Ensuite toute déclaration inline sera prioritaire ;
  • Ensuite c'est le sélecteur avec le plus grand nombre d'identifiants qui sera prioritaire ;
  • Ensuite c'est le sélecteur avec le plus grand nombre de classes ou pseudo-classes qui sera prioritaire ;
  • Ensuite c'est le sélecteur avec le plus grand nombre d'éléments ou pseudo-éléments qui sera prioritaire ;
  • Et enfin c'est les règles écrites en dernier qui seront prioritaires sur celles écrites en premier.

En conclusion, on en déduit les bonnes pratiques suivantes :

  • Toujours ordonner vos règles CSS selon l'ordre de spécificité de leur sélecteurs
  • Inscrire la valeur de spécificité dans un commentaire au-dessus de chaque sélecteur
  • Ne plus utiliser d'identifiants dans vos sélecteurs CSS
  • Dès que possible, remplacer les sélecteurs trop complexes par la création de nouvelles classes
  • Dès que possible remplacer les sélecteurs du DOM (  > ~ +) par la création de nouvelles classes
  • Éviter au maximum l'utilisation de styles inline
  • Ne JAMAIS utiliser !important
  • Si vraiment vous êtes obligé d'utiliser !important : placez les règles qui s'en servent à la fin de votre code, en dernier

Mais ta solution là, ça va pas nous faire créer trop de classes ?

Elle va vous en faire créer beaucoup plus qu'avant oui. Mais pas trop. Les navigateurs savent gérer, même avec un nombre de classes très conséquent, faites-leur confiance. En contrepartie vous produisez un code CSS qui a tout pour évoluer de façon propre, être toujours maintenable d'ici plusieurs années malgré beaucoup d'ajouts et de modifications, capable d'encaisser des montées en charge, capable d'être maintenu par plusieurs développeurs s'ils respectent tous les mêmes bonnes pratiques.

Enfin on pourrait rajouter à tout ceci quelques bonnes pratiques plus génériques et évidentes :

  • Que vous préfériez une approche par composants ou une approche plus fonctionnelle à la Tailwind, choisissez une syntaxe pour vos classes et respectez-la. Vous pouvez vous inspirer de ce qui existe : BEM, Atomic Design, etc
  • De façon plus générale faites preuve de logique pour nommer vos classes, pensez-les comme les plus descriptives et réutilisables possible
  • Utilisez un préfixe par projet commun à toutes vos classes, ex: masta_header, masta_button, etc. Cela vous évitera les conflits avec le CSS des différentes librairies que vous voudrez utiliser
  • Placez vos @font-face et vos @keyframe en tout début de fichier
  • Placez vos @media en toute fin de fichier, et à l'intérieur appliquez de nouveau un tri de vos règles par valeur de spécificité

Avec tout ça, vous avez tout ce qu'il vous faut pour écrire le code CSS le plus propre et maintenable qui soit. A vous ensuite d'avoir rigueur et méthode afin d'appliquer ces bonnes pratiques sur tous vos projets.

Par expérience, je l'ai fait, testé en production, ça fonctionne, ça change tout. Depuis que j'ai compris tout ça, je sens que je maîtrise CSS bien mieux qu'avant, ce qui a fait ses preuves dans la plupart de mes projets. J'espère que ce sera également votre cas dès à présent 😎

Tim Berners-Lee l'inventeur du Web
Grâce à ce tuto Tim maîtrise le web à la perfection