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

Découvrez comment utiliser @scope pour sélectionner des éléments uniquement dans un sous-arbre limité de votre DOM.

Navigateurs pris en charge

  • Chrome : 118.
  • Edge : 118.
  • Firefox : derrière un indicateur.
  • Safari : 17.4.

Source

L'art délicat d'écrire des sélecteurs CSS

Lorsque vous écrivez des sélecteurs, vous pouvez être déchiré entre deux mondes. D'une part, vous devez être assez précis quant aux éléments que vous sélectionnez. En revanche, vous souhaitez que vos sélecteurs restent faciles à remplacer et ne soient pas étroitement liés à la structure DOM.

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

  • Ce sélecteur présente une spécificité assez élevée de (0,3,1), ce qui le rend difficile à remplacer à mesure que votre code se développe.
  • En s'appuyant sur le combinateur d'enfant direct, il est étroitement associé à la structure DOM. Si le balisage change, vous devez également modifier votre CSS.

Vous ne souhaitez pas non plus écrire uniquement img comme sélecteur pour cet élément, car cela sélectionnerait tous les éléments image de votre page.

Trouver le juste équilibre dans ce domaine est souvent un défi de taille. Au fil des ans, certains développeurs ont trouvé des solutions et des solutions de contournement pour vous aider dans ce genre de situations. Exemple :

  • Des méthodologies telles que BEM vous obligent à attribuer à cet élément une classe card__img card__img--hero pour réduire la spécificité tout en vous permettant d'être précis dans ce que vous sélectionnez.
  • Les solutions basées sur JavaScript telles que le CSS avec portée ou les composants stylisés réécrivent tous vos sélecteurs en ajoutant des chaînes générées de manière aléatoire (sc-596d7e0e-4, par exemple) pour les empêcher de cibler des éléments situés de l'autre côté de votre page.
  • Certaines bibliothèques abolissent 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'avez besoin d'aucun de ces éléments ? Et si le CSS vous permettait d'être à la fois précis sur les éléments que vous sélectionnez, sans vous obliger à écrire des sélecteurs de grande spécificité ou des éléments étroitement liés à votre DOM ? C'est là que @scope entre en jeu. Il vous permet de sélectionner des éléments uniquement dans une sous-arborescence de votre DOM.

Présentation de @scope

Avec @scope, vous pouvez limiter la portée 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 à cibler. Avec une racine de champ d'application définie, les règles de style contenues (appelées règles de style de portée) ne peuvent sélectionner que dans ce sous-arbre limité du DOM.

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

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

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

Pour empêcher la sélection des éléments <img> situés dans la zone de contenu de la fiche (.card__content), vous pouvez rendre le sélecteur img plus spécifique. Vous pouvez également utiliser le fait que la règle at @scope 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 des limites supérieure et inférieure) est souvent appelé champ d'application de l'anneau.

Sélecteur :scope

Par défaut, toutes les règles de style appliquées sont relatives à 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 :scope en préfixe. Vous pouvez également ajouter le sélecteur & en amont, à partir du 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 faire référence à 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 limitées elles-mêmes ne peuvent pas échapper la sous-arborescence. Les sélections telles que :scope + p ne sont pas valides, car la fonction tente de sélectionner des éléments qui ne sont pas concernés par 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 est toujours (0,0,1).

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

La spécificité de :scope est celle d'une pseudo-classe régulière, à 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 sur le sélecteur utilisé pour la racine de champ d'application, encapsulé dans un sélecteur :is(). En fin de compte, 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 selon les règles de spécificité :is(): la spécificité de & est celle de son argument le plus spécifique.

Appliquée à 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). En combinant cela avec la spécificité de img, qui est (0,0,1), vous obtenez (1,0,1) comme spécificité pour 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 & présentent une autre différence : :scope représente la racine de portée correspondante, tandis que & représente le sélecteur utilisé pour faire correspondre la racine de portée.

C'est pourquoi vous pouvez utiliser & plusieurs fois. Contrairement à :scope, que vous ne pouvez utiliser qu'une seule fois, 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 */
  }
  :scope :scope { /* ❌ Does not work */
    
  }
}

Champ d'application sans prélude

Lorsque vous écrivez des styles intégrés avec l'élément <style>, vous pouvez limiter le champ d'application des 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 de portée ne ciblent que les éléments situés dans le div avec le nom de classe card__header, car div est l'élément parent de l'élément <style>.

@scope dans la cascade

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

Visualisation de la cascade CSS.

Conformément aux spécifications:

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

Cette nouvelle étape est utile lorsque vous imbriquez plusieurs variantes d'un composant. Prenons cet exemple, 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>

Lors de l'affichage de ce petit extrait de balisage, le troisième lien sera 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 le gagnant. Il voit que .dark a a été déclaré en dernier, il va donc gagner en vertu 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 avec portée ont la même spécificité, le critère de proximité de la portée entre en action. Il pondère les deux sélecteurs en fonction de leur proximité avec leur racine de champ d'application. Pour ce troisième élément a, il n'y a qu'un seul saut vers la racine de champ d'application .light, mais deux vers celle de .dark. Par conséquent, le sélecteur a dans .light l'emporte.

Remarque finale: Isolation du sélecteur, et non de style

Notez que @scope limite la portée des sélecteurs, mais n'offre pas d'isolation de style. Les propriétés qui héritent des enfants hériteront toujours, au-delà de la limite inférieure de la @scope. La propriété color en est un exemple. Lorsque vous déclarez un élément dans le champ d'application d'un donut, color hérite toujours des enfants dans le trou du donut.

@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 par rustam burkhanov sur Unsplash)