Limiter la portée de vos sélecteurs avec l'attribut CSS @scope at-rule

Découvrez comment utiliser @scope pour ne sélectionner des éléments que dans une sous-arborescence limitée de votre DOM.

Navigateurs pris en charge

  • 118
  • 118
  • x
  • x

La délicatesse d'écriture des sélecteurs CSS

Lorsque vous écrivez des sélecteurs, vous pouvez être tiraillé entre deux mondes. D'une part, vous devez être assez précis sur les éléments que vous sélectionnez. En revanche, vous devez faire en sorte que vos sélecteurs restent faciles à remplacer et ne soient pas étroitement liés à la structure DOM.

Par exemple, lorsque vous souhaitez sélectionner "l'image héros dans la zone de contenu du composant de la fiche" (ce qui est une sélection d'éléments plutôt spécifique), vous ne souhaiterez probablement pas écrire un sélecteur comme .card > .content > img.hero.

  • Ce sélecteur ayant une spécificité assez élevée de (0,3,1), il est difficile de le remplacer à mesure que votre code prend de l'ampleur.
  • En s'appuyant sur le combinateur enfant direct, il est étroitement lié à la structure DOM. Si le balisage est modifié, vous devez également modifier votre CSS.

Cependant, il ne faut pas non plus écrire img comme sélecteur pour cet élément, car cela sélectionnerait tous les éléments d'image de votre page.

Trouver le juste équilibre est souvent un véritable défi. Au fil des ans, certains développeurs ont inventé des solutions et des solutions pour vous aider dans de telles situations. Exemple :

  • Des méthodologies telles que le BEM vous obligent à attribuer à cet élément une classe card__img card__img--hero afin de maintenir un niveau de spécificité faible tout en vous permettant d'être précis dans ce que vous sélectionnez.
  • Les solutions basées sur JavaScript telles que le CSS cloisonné ou les composants stylisés réécrivent tous vos sélecteurs en ajoutant des chaînes générées de façon aléatoire (comme sc-596d7e0e-4) à vos sélecteurs pour les empêcher de cibler des éléments situés à l'autre côté de votre page.
  • Certaines bibliothèques suppriment même complètement les sélecteurs et vous obligent à placer les déclencheurs de style directement dans le balisage lui-même.

Mais que faire si vous n'en aviez pas besoin ? Et si le CSS vous permettait d'être à la fois assez spécifique concernant les éléments que vous sélectionnez, sans vous obliger à écrire des sélecteurs très spécifiques ou des sélecteurs étroitement liés à votre DOM ? C'est là que @scope entre en jeu, qui vous offre un moyen de sélectionner des éléments uniquement dans une sous-arborescence de votre DOM.

Présentation de @scope

@scope vous permet de limiter la couverture de vos sélecteurs. Pour ce faire, définissez la racine de champ d'application, qui détermine la limite supérieure de la sous-arborescence que vous souhaitez cibler. Avec un ensemble de racines de champ d'application, les règles de style contenues (appelées règles de style étendues) ne peuvent sélectionner que cette sous-arborescence limitée du DOM.

Par exemple, pour ne cibler que les éléments <img> du composant .card, vous devez définir .card comme racine de champ d'application de la règle @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

La règle de style de portée img { … } ne peut sélectionner de fait que les éléments <img> qui sont dans le champ d'application de l'élément .card correspondant.

Pour éviter que les éléments <img> situés dans la zone de contenu de la fiche (.card__content) ne soient sélectionnés, vous pouvez rendre le sélecteur img plus spécifique. Une autre façon de procéder consiste à utiliser le fait que la règle @scope au niveau de la règle accepte également une limite de champ d'application qui détermine la limite inférieure.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

Cette règle de style délimitée ne cible que les éléments <img> placés entre les éléments .card et .card__content dans l'arborescence ancêtre. Ce type de champ d'application (avec une limite supérieure et inférieure) est souvent appelé champ d'application en forme de beignet.

Sélecteur :scope

Par défaut, toutes les règles de style de portée sont liées à la racine de champ d'application. Il est également possible de cibler l'élément racine de champ d'application lui-même. Pour ce faire, utilisez le sélecteur :scope.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Le préfixe :scope est implicitement ajouté aux sélecteurs inclus dans les règles de style délimitées. Si vous le souhaitez, vous pouvez être explicite à ce sujet en ajoutant vous-même le préfixe :scope. Vous pouvez également ajouter le sélecteur & au début, sur la page Imbrication CSS.

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

Une limite de champ d'application peut utiliser la pseudo-classe :scope pour exiger une relation spécifique avec la racine de champ d'application:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

Une limite de champ d'application peut également référencer des éléments en dehors de sa racine de champ d'application à l'aide de :scope. Exemple :

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Notez que les règles de style délimitées ne peuvent pas échapper à la sous-arborescence. Les sélections telles que :scope + p ne sont pas valides, car cette méthode tente de sélectionner des éléments qui ne sont pas inclus dans le champ d'application.

@scope et spécificité

Les sélecteurs que vous utilisez dans le prélude pour @scope n'affectent pas la spécificité des sélecteurs contenus. Dans l'exemple ci-dessous, la spécificité du sélecteur img reste (0,0,1).

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

La spécificité de :scope est celle d'une pseudo-classe standard, à savoir (0,1,0).

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

Dans l'exemple suivant, & est réécrit en interne dans le sélecteur utilisé pour la racine de champ d'application, encapsulé dans un sélecteur :is(). À la fin, le navigateur utilisera :is(#sidebar, .card) img comme sélecteur pour effectuer la mise en correspondance. Ce processus est appelé désucrage.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

Étant donné que & est désucré à l'aide de :is(), la spécificité de & est calculée en suivant les règles de spécificité :is(): la spécificité de & correspond à son argument le plus spécifique.

Appliqué à cet exemple, la spécificité de :is(#sidebar, .card) est celle de son argument le plus spécifique, à savoir #sidebar, et devient donc (1,0,0). Si vous combinez cela avec la spécificité de img ((0,0,1)), vous obtenez (1,0,1) comme spécificité de l'ensemble du sélecteur complexe.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}

Différence entre :scope et & dans @scope

Outre les différences de calcul de la spécificité, :scope et & représentent une autre différence : :scope représente la racine de champ d'application correspondante, tandis que & représente le sélecteur utilisé pour faire correspondre la racine de champ d'application.

C'est pourquoi vous pouvez utiliser & plusieurs fois. Cela diffère de :scope, que vous ne pouvez utiliser qu'une seule fois, car vous ne pouvez pas faire correspondre une racine de champ d'application dans une racine de champ d'application.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ Does not work */
    …
  }
}

Portée sans prélude

Lorsque vous écrivez des styles intégrés avec l'élément <style>, vous pouvez limiter les règles de style à l'élément parent englobant de l'élément <style> en ne spécifiant aucune racine de champ d'application. Pour ce faire, omettez le prélude de @scope.

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

Dans l'exemple ci-dessus, les règles limitées ne ciblent que les éléments du div dont le nom de classe est card__header, car div est l'élément parent de l'élément <style>.

@scope dans la cascade

Dans le cascade CSS, @scope ajoute également un nouveau critère: la proximité de la portée. L'étape intervient après la spécificité, mais avant l'ordre d'apparition.

Visualisation de la cascade CSS

Conformément à la spécification:

Lorsque vous comparez des déclarations qui apparaissent dans des règles de style ayant différentes racines de champ d'application, la déclaration qui comporte le moins de sauts d'éléments générations ou frères entre la racine de champ d'application et l'objet de la règle de style de portée l'emporte.

Cette nouvelle étape s'avère pratique lorsque vous imbriquez plusieurs variantes d'un composant. Prenons l'exemple suivant, qui n'utilise pas encore @scope:

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

Lorsque vous consultez ce balisage, le troisième lien est white au lieu de black, même s'il s'agit d'un enfant d'un div auquel la classe .light est appliquée. Cela est dû au critère d'ordre d'apparition que la cascade utilise ici pour déterminer la variante la plus performante. Comme il constate que la valeur .dark a a été déclarée en dernier, elle gagnera à partir de la règle .light a.

Le critère de proximité de champ d'application permet désormais de résoudre ce problème:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Étant donné que les deux sélecteurs a de portée ont la même spécificité, le critère de proximité de champ d'application est utilisé. Elle pondère les deux sélecteurs en fonction de leur proximité par rapport à leur racine de champ d'application. Pour ce troisième élément a, il ne s'agit que d'un saut vers la racine de champ d'application .light, mais de deux sauts vers la racine de champ d'application .dark. Par conséquent, le sélecteur a dans .light l'emportera.

Remarque finale: Isolation du sélecteur, et non isolation des styles

Il est important de noter que @scope limite la portée des sélecteurs et n'offre pas d'isolation de style. Les propriétés qui héritent des éléments enfants continuent d'hériter, au-delà de la limite inférieure de @scope. L'une de ces propriétés est color. Lorsque vous déclarez qu'il se trouve dans le champ d'application de l'anneau, le color hérite toujours des enfants situés à l'intérieur du trou de l'anneau.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

Dans l'exemple ci-dessus, l'élément .card__content et ses enfants ont une couleur hotpink, car ils héritent de la valeur de .card.

(Photo de couverture de rustam burkhanov sur Unsplash)