css et mises en page

CSS @layer : comment les cascade layers mettent fin à la guerre de spécificité

CSS @layer : comment les cascade layers mettent fin à la guerre de spécificité

Vous avez un composant de carte issu d'une bibliothèque de design system : .card .card__header .card__title { font-size: 18px; } – trois classes dans le sélecteur, car c'est ainsi que l'auteur du composant l'a imbriqué. Vous voulez l'écraser avec une seule classe utilitaire, .text-lg { font-size: 20px; }, ajoutée dans le HTML après les classes de la carte. Ça ne fonctionne pas. Le titre reste obstinément à 18px, alors même que .text-lg apparaît plus tard dans le code, ce qui laisserait intuitivement penser que « la plus récente devrait gagner ». Ce n'est pas un bug du navigateur – trois classes dans un sélecteur battent une seule classe, indépendamment de l'ordre dans le fichier. C'est ainsi que fonctionne la spécificité depuis 1996, et c'est voulu.

Le contournement classique consiste à augmenter la spécificité de sa propre règle – .card .text-lg, ou, quand cela ne suffit pas, !important. Les deux solutions fonctionnent ponctuellement et se dégradent systémiquement : chaque fois que quelqu'un dans l'équipe ajoute une règle avec une spécificité encore plus élevée (ou un !important de plus), tout le reste de la feuille de style devient imprévisible. C'est précisément ce problème – pas esthétique, mais architectural – que résolvent les CSS Cascade Layers.

Un nouvel axe de résolution des conflits, pas une nouvelle façon d'augmenter la priorité

La chose essentielle à comprendre : @layer n'est pas un nouvel outil pour augmenter la priorité d'une règle, comme !important ou l'ajout d'une classe supplémentaire au sélecteur. C'est une étape distincte de l'algorithme de cascade, résolue avant la spécificité, pas à côté d'elle. L'ordre complet dans lequel le navigateur résout un conflit entre deux règles CSS ressemble (en simplifiant en omettant l'origine) à ceci :

  1. La couche (@layer) – une règle d'une couche déclarée plus tard l'emporte sur une règle d'une couche déclarée plus tôt, indépendamment de la spécificité des sélecteurs à l'intérieur.
  2. La spécificité – ne tranche que lorsque les deux règles appartiennent à la même couche (ou qu'aucune des deux n'appartient à une couche).
  3. L'ordre dans la source – le dernier arbitre, quand les deux critères précédents donnent match nul.

Cela permet à une seule classe .text-lg dans une couche ultérieure de battre un sélecteur à trois classes dans une couche antérieure – car l'affrontement n'atteint jamais l'étape de comparaison des spécificités. La couche tranche le sujet en amont.

css

/* L'ordre des couches se déclare une fois, en haut – il n'a pas besoin
de correspondre à l'ordre dans lequel leur contenu apparaît réellement dans les fichiers */
@layer reset, base, components, utilities;
@layer components {
.card .card__header .card__title {
font-size: 18px;
}
}
@layer utilities {
.text-lg {
font-size: 20px;
}
}

Cette déclaration vide @layer reset, base, components, utilities; en haut du fichier n'est pas une formalité – c'est l'établissement explicite de l'ordre de priorité, avant même que l'une de ces couches ne reçoive un contenu réel. Grâce à cela, l'ordre des couches est indépendant de l'ordre des imports ou des fichiers – components peut physiquement se trouver dans le code avant utilities, et utilities l'emportera quand même, car cet ordre a été déclaré dès le départ.

Exemple pratique : reset, composants et utilitaires qui ne se battent jamais entre eux

En pratique, ce motif – le reset en dessous des composants, les composants en dessous des classes utilitaires – correspond exactement à la façon dont fonctionne, par exemple, le système de couches de Tailwind depuis la version 3.1 : une classe utilitaire doit toujours l'emporter sur une règle de composant, quel que soit le nombre de classes que possède le sélecteur de ce composant, car c'est précisément la raison d'être des classes utilitaires.

css

@layer reset, base, components, utilities;
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
color: #1a1a1a;
}
}
@layer components {
.card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
}
.card .card__header .card__title {
font-size: 18px;
font-weight: 600;
}
}
@layer utilities {
.text-lg {
font-size: 20px !important;
}
.mt-4 {
margin-top: 16px;
}
}

Remarquez que dans la couche utilities, le !important sur .text-lg est ici superflu – la couche assure déjà la priorité, sans avoir besoin de recourir à l'arme la plus lourde du CSS. C'est une bonne illustration de la raison pour laquelle les cascade layers réduisent réellement le nombre de !important dans le code : vous cessez d'en avoir besoin comme contournement de la spécificité, puisque vous disposez d'un outil conçu exactement pour cette tâche.

Ce même mécanisme résout un second problème, tout aussi fréquent – une bibliothèque CSS externe dont vous ne contrôlez pas les sélecteurs, et dont vous voulez que la priorité soit toujours inférieure à vos propres styles, sans augmenter la spécificité de vos propres règles :

css

@import url("some-ui-library.css") layer(vendor);
@layer vendor, base, components, utilities;

Toute la bibliothèque atterrit dans la couche vendor, placée tout au début de l'ordre – donc vos couches base, components et utilities la battront toujours, peu importe à quel point cette bibliothèque construit ses sélecteurs de façon agressive.

Le piège qui brise l'intuition : un style sans couche bat toutes les couches

C'est le détail le plus souvent négligé, et pourtant le plus lourd de conséquences de tout le mécanisme : une règle qui n'appartient à aucune couche l'emporte sur une règle dans n'importe quelle couche – pour les déclarations ordinaires, non marquées !important. Elle ne « perd pas parce qu'elle est dans une couche invisible en fin de liste » – elle gagne toujours, inconditionnellement, contre toute règle en couche, même si cette dernière se trouve dans une couche déclarée comme priorité la plus élevée.

css

@layer utilities {
.text-lg {
font-size: 20px;
}
}
/* Cette règle, bien que n'appartenant à aucune couche et ressemblant à
un override isolé et anodin, BAT .text-lg ci-dessus,
peu importe où elle se trouve dans le fichier */
.title {
font-size: 16px;
}

Ce comportement est intentionnel – il rend sûre l'adoption progressive des cascade layers dans un grand projet existant : tout l'« ancien » CSS qui n'a pas encore été affecté à une couche conserve par défaut la priorité la plus élevée, il ne se met donc pas soudainement à perdre face aux couches nouvellement ajoutées. Mais ce même comportement est la source des bugs les plus déroutants dans les équipes qui adoptent @layer de façon partielle : une seule règle laissée par accident hors de toute couche – une faute de frappe, un import oublié, un bloc <style> injecté par une bibliothèque tierce – battra tout le système de priorités soigneusement conçu, sans le moindre avertissement. Conséquence pratique : si vous optez pour les cascade layers, mettez systématiquement tout dans une couche ou une autre, y compris le reset et les styles de base – ne laissez aucune règle « à l'extérieur », car c'est elle, pas la couche utilities, qui gagnera chaque conflit.

Second piège : !important inverse l'ordre des couches

Si vous recourez à !important à l'intérieur d'une couche, l'ordre de priorité entre les couches s'inverse. Pour les déclarations ordinaires, la couche déclarée plus tard l'emporte. Pour les déclarations avec !important – c'est la couche déclarée plus tôt qui l'emporte.

css

@layer reset, components;
@layer reset {
.card { padding: 0 !important; }
}
@layer components {
.card { padding: 16px !important; }
}
/* Le reset gagne (0px), car avec !important, la priorité revient à
la couche déclarée en premier – l'inverse exact de la règle
qui s'applique sans !important */

Ce n'est pas un caprice de la spécification – c'est l'extension cohérente d'une règle qui existait déjà pour les origines (les feuilles de style !important de l'utilisateur ont toujours battu les !important de l'auteur de la page, à l'inverse de ce qui se passe avec les déclarations ordinaires). Conclusion pratique : !important à l'intérieur d'un système de couches peut surprendre même quelqu'un qui comprend bien l'ordre habituel de @layer – une raison de plus de traiter !important comme quelque chose que l'on utilise consciemment et rarement, pas comme un moyen par défaut de remporter un affrontement.

Pièges et bonnes pratiques

  • Zéro règle hors des couches, si le système doit être prévisible. Comme montré plus haut, même une seule règle sans @layer bat tout le système de couches pour les déclarations ordinaires – la cohérence prime sur la commodité d'ajouter « juste une petite règle » au vol.
  • La spécificité s'applique toujours à l'intérieur d'une même couche. @layer élimine les guerres de spécificité entre les couches, mais à l'intérieur d'une même couche, deux règles tranchent toujours le conflit comme d'habitude – un nommage de classes sensé et l'évitement de sélecteurs profondément imbriqués restent payants.
  • Déclarez l'ordre des couches explicitement, tout en haut, avec un @layer nom1, nom2, ...; vide. Cela vous libère de la dépendance à l'ordre des imports de fichiers – l'ordre de priorité est fixé une fois, lisiblement, à un seul endroit, indépendamment de l'ordre dans lequel les différents fichiers CSS se chargent effectivement ensuite.
  • La compatibilité des navigateurs n'est plus une raison de prudence. Les Cascade Layers ont atteint le statut Baseline (largement disponible) dès 2022 – Chrome 99, Firefox 97, Safari 15.4 – on peut donc les adopter sans @supports dans tout nouveau projet.

Conclusion

@layer ne concurrence pas la spécificité – il agit à une étape complètement différente de l'algorithme de cascade, en amont, ce qui permet à une seule classe utilitaire de battre consciemment et de façon prévisible un sélecteur à trois classes imbriquées, sans augmenter de priorité ni recourir à !important. C'est un véritable outil architectural pour séparer le reset, les styles de base, les composants et les utilitaires en couches à l'ordre d'importance explicitement déclaré – mais deux de ses règles brisent suffisamment l'intuition pour valoir la peine d'être retenues à part : un style hors de toute couche bat chaque couche, et !important à l'intérieur des couches inverse leur ordre de priorité. Ignorer l'un ou l'autre de ces deux faits ne se traduira pas par une erreur dans la console – cela se traduira par du CSS qui se comporte différemment de ce que suggérerait l'ordre apparemment logique des couches dans le fichier.