La gestion de la portée des styles est un défi historique en CSS. Des méthodologies comme BEM aux solutions techniques comme le Shadow DOM ou les CSS Modules, nous avons toujours cherché à éviter que le style d'un composant ne se propage sur ses voisins. L'arrivée de la règle @scope change la donne en offrant une solution native, flexible et puissante.
Présentation rapide de @scope
La règle @scope permet de cibler des éléments dans un fragment spécifique du DOM.
Dans cet exemple simple, seuls les <h2> à l'intérieur des éléments portant la classe .card seront colorés en hotpink. Les styles n'affecteront pas les autres <h2> de la page :
@scope (.card) {
/* Cible les h2 uniquement dans .card */
h2 {
color: hotpink;
}
}
Dans cet autre exemple sont ciblés uniquement les <h2> à l'intérieur des éléments portant la classe .card contenant un élément .title :
@scope (.card:has(.title)) {
/* Cible les h2 uniquement dans .card qui contient .title */
h2 {
color: hotpink;
}
}
Compatibilité navigateurs
Voici le tableau de compatibilité de @scope, que vous pouvez également consulter sur Caniuse.com :
Le sélecteur :scope et l'esperluette &
À l'intérieur d'un bloc @scope, le navigateur a besoin d'un moyen de désigner l'élément racine lui-même (celui qui porte la classe ou l'attribut de départ). Il existe deux façons de procéder :
:scope: Une pseudo-classe qui référence logiquement l'élément racine. C'est le cœur technique du dispositif.&(Esperluette) : Comme dans le nesting classique, elle permet de coller des états à la racine (ex:&:hover).
Dans la plupart des cas, l'usage du & pour les états interactifs est recommandé pour sa simplicité.
Par exemple, si on souhaite que son composant .card ait un fond gris, mais que ses éléments internes aient leurs propres styles, on va se servir de :scope :
@scope (.card) {
/* Cible l'élément .card lui-même */
:scope {
background: var(--surface);
border: 1px solid var(--border-light);
}
}
Pour les éléments hautement réutilisables comme les boutons, :scope associé à & (esperluette) permettent de centraliser la logique interactive (hover, focus, active) en utilisant les variables CSS.
@scope (.btn) {
:scope {
/* Variables par défaut */
--button-background-color: var(--form-background, Field);
--button-text-color: var(--on-form, ButtonText);
background-color: var(--button-background-color);
color: var(--button-text-color);
transition: all var(--transition-duration);
}
/* Logique interactive unique pour toutes les variantes */
&:hover, &:focus-visible {
background-color: oklch(from var(--button-background-color) calc(l * 0.9) c h);
}
&:disabled {
opacity: 0.8;
cursor: not-allowed;
}
/* Variantes : On ne change que les valeurs, pas la logique */
&.btn-primary {
--button-background-color: var(--primary);
--button-text-color: var(--on-primary);
}
}
Mais pourquoi ne pas se contenter de .card .title ou .card > .title ?
C'est bien vrai ça, on pourrait croire que @scope ne sert qu'à nous compliquer la vie. Alors que… pas du tout !
- La proximité l'emporte sur l'ordre : en CSS standard, si deux sélecteurs ont la même spécificité, le dernier écrit gagne. Avec
@scope, c'est la racine la plus proche de l'élément dans le DOM qui l'emporte. C'est idéal pour les thèmes imbriqués. - La spécificité reste basse :
.card .titlea une spécificité de (0,2,0). À l'intérieur d'un@scope, le sélecteur.titlereste à (0,1,0), facilitant les surcharges sans utiliser!important. - Robustesse face au DOM : le sélecteur d'enfant direct (
>) est fragile. Si on ajoute unedivintermédiaire pour du layout, le style casse.@scopetolère n'importe quelle profondeur tant que la limite n'est pas franchie.
En outre, grâce à la priorité par proximité, @scope permet d'imbriquer des thèmes sans conflit, peu importe l'ordre des CSS.
@scope (.theme-dark) {
:scope { background: #1a1a1a; color: white; }
.title { color: var(--color-pink-300); }
}
@scope (.theme-light) {
:scope { background: white; color: #1a1a1a; }
.title { color: var(--color-blue-600); }
}
Permettre un périmètre sélectif ("Donut Hole")
Contrairement aux sélecteurs descendants classiques, @scope introduit deux concepts majeurs :
- La racine du scope (limite haute) : l'élément à partir duquel les règles s'appliquent.
- La limite du scope (limite basse, optionnelle) : l'élément où les règles s'arrêtent.
Dans l'exemple suivant, le navigateur cherche .card pour y colorer les h2 en rose. Cependant, dès qu'il rencontre un élément .content, il cesse d'appliquer les styles à ses descendants :
@scope (.card) to (.content) {
/* Cible les .title dans .card mais s'arrête à .content */
.title {
color: hotpink;
}
}
C'est l'atout majeur de @scope : la capacité de définir un périmètre d'exclusion via la clause to. Cela permet de protéger des zones de contenu riche (WYSIWYG, slots) ou des composants imbriqués qui ne doivent pas être affectés par les styles du parent.
Mais alors c'est mieux que BEM ?
BEM est une convention efficace mais verbeuse, et elle repose entièrement sur votre discipline de nommage et nécessite des noms de classes uniques (.card__title, .hero__title--alternate).
Avec @scope, vous pouvez garder des noms plus simples et réutilisables, tout en garantissant que les styles restent confinés à leur contexte : le HTML est épuré des préfixes répétitifs et grâce au to (.content), un titre de card situé dans un élément .content sera jamais affecté par le style de la carte, car il est "hors limite".
L'adoption de @scope marque une étape vers la fin de l'ère "tout BEM" au profit d'une approche plus sémantique et structurelle.
Dans un projet concret, BEM, @layer et @scope peuvent cependant coexister :
- BEM est une convention de nommage pour humains (organisation visuelle).
- @layer gère la priorité des sources (fichiers, frameworks).
- @scope est une barrière de sécurité technique (structure du DOM).
Et Tailwind dans tout ça ?
On ne va pas se mentir : l'intégration de @scope dans un projet utilisant déjà un framework utilitaire comme Tailwind peut sembler redondante au premier abord.
Il existe cependant quelques cas d'usage où @scope peut apporter une valeur ajoutée significative, notamment pour les composants complexes et les zones de contenu dynamique qui ne sont pas facilement gérables avec les classes utilitaires seules.
Par exemple, dans Tailwind, styler du HTML brut (venant d'un CMS) nécessite souvent l'utilisation du plugin @tailwindcss/typography (classe .prose). Cependant, ce plugin est parfois trop rigide. Avec @scope, on peut créer des "bulles" de styles spécifiques pour son contenu sans polluer le reste du site et sans créer de classes complexes :
@layer components {
@scope (.prose-custom) to (.content) {
/* On style les balises nues uniquement dans ce contexte */
h2 { @apply text-2xl font-bold mb-4 text-primary; }
p { @apply leading-relaxed mb-6; }
/* Les limites protègent les composants Tailwind imbriqués */
}
}
En résumé
Si l'on devait résumer les avantages de @scope en quelques points clés :
- Allègement du HTML : plus besoin de classes à rallonge comme
.card-header__title. On utilise des classes simples (.title,.media,.header,.content) et faciles à retenir. - Proximité l'emporte sur l'ordre : les styles s'appliquent en fonction de la proximité dans le DOM, pas de l'ordre d'apparition dans le CSS. Idéal pour les thèmes imbriqués.
- Isolation par limites : on bénéficie de la clause
topour définir une limite haute et basse et garantir que les utilitaires appliqués à un composant enfant ne seront jamais écrasés par les styles du parent. - Débogage facilité : dans l'inspecteur du navigateur, la règle
@scopeapparaît clairement, permettant de voir immédiatement quelle racine applique le style, plutôt que de chercher l'origine dans une pile de sélecteurs descendants complexes. - Spécificité contrôlée : contrairement aux sélecteurs descendants classiques (
.card .btn) qui augmentent la spécificité,@scopemaintient une spécificité faible. Cela permet à ses classes utilitaires (ex:p-4) de rester prioritaires et faciles à surcharger si besoin.
Si l'on ne devait en retenir qu'un seul, il s'agirait sans aucun doute de Conflits de nommage réduits. En effet, des noms de classes génériques comme .title peuvent être utilisées dans plusieurs scopes sans risque de conflit, car elles ne s'appliquent qu'à leur propre scope. Et tout le monde sait que nommer les choses c'est… compliqué.
Conclusion
@scope ne remplace pas les outils actuels, il complète avantageusement l'outillage natif déjà mis en place par CSS (cascade, @layer, :where(), etc.).
Totalement inadapté pour les styles globaux (reset, typographie de base, utilitaires) qui doivent se propager partout, il devient en revanche un allié précieux pour les styles de composants et les zones de contenu dynamique.
Utilisons-le dans notre @layer(components) pour isoler nos "organismes" (cartes, headers, modales). C'est la fin du "CSS qui bave" et le début d'une architecture plus proche de la structure réelle de vos pages.
Note : Le support navigateur est désormais excellent dans les versions récentes de Firefox, Chrome, Edge et Safari. Pour les projets nécessitant un support legacy, une stratégie de Progressive Enhancement via @supports (scope: html) est préconisée.
Commenter
Vous devez être inscrit et identifié pour utiliser cette fonction.
Connectez-vous (déjà inscrit)
Pas encore inscrit ? C'est très simple et gratuit.