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 une sous-arborescence limitée de votre DOM.

Navigateurs pris en charge

  • 118
  • 118
  • x
  • 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, lorsque vous souhaitez sélectionner "l'image de héros dans la zone de contenu du composant Carte", qui est une sélection d'éléments plutôt spécifique, vous ne souhaiterez probablement pas écrire un sélecteur tel que .card > .content > img.hero.

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

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

Il est souvent difficile de trouver le bon équilibre. Au fil des ans, certains développeurs ont proposé des solutions et des solutions de contournement pour vous aider dans de telles situations. Exemple :

  • Des méthodologies telles que BEM imposent d'attribuer à cet élément une classe de card__img card__img--hero pour limiter 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 délimité 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 (comme sc-596d7e0e-4) à vos sélecteurs afin qu'ils ne ciblent pas les éléments situés à l'autre bout de la 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.

Et si vous n'en aviez pas besoin ? 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

@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 à cibler. Avec un ensemble racine de champ d'application, les règles de style contenues (appelées règles de style limitées) ne peuvent être sélectionnées qu'à partir de cette sous-arborescence limitée du DOM.

Par exemple, pour ne cibler que les éléments <img> dans le 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 délimitée img { … } ne peut sélectionner que des éléments <img> qui sont dans le champ d'application 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. Une autre façon de procéder consiste à utiliser le fait que l'élément @scope at-rule 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 le préciser de manière explicite en ajoutant vous-même le préfixe :scope. Vous pouvez également ajouter un préfixe au sélecteur &, dans Imbriquer les 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 situés 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 de @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 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 sur le sélecteur utilisé pour la racine de champ d'application, encapsulé dans un sélecteur :is(). Au final, 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 champ d'application correspondante, tandis que & représente le sélecteur utilisé pour correspondre à la racine de champ d'application.

Pour cette raison, il est possible d'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 */
  }
  :scope :scope { /* ❌ 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 appliquées ne ciblent que les éléments situés à l'intérieur de 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: scoping proximity (proximité dans le champ d'application). L'étape vient 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 ayant le moins de sauts de génération ou d'élément frère entre la racine de champ d'application et l'objet de la règle de style délimitée l'emporte.

Cette nouvelle étape s'avère particulièrement 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 la configuration gagnante. Il voit que .dark a a été déclaré en dernier, il va donc gagner grâce à la règle .light a

Le problème est maintenant résolu avec le critère de proximité de champ d'application:

@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 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 saut vers la racine de champ d'application .light, mais deux vers celui de .dark. Par conséquent, le sélecteur a dans .light l'emporte.

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

Remarque importante : @scope limite la portée des sélecteurs et ne permet pas d'isoler les styles. Au-delà de la limite inférieure de @scope, les propriétés qui héritent des droits jusqu'aux enfants en héritent. L'une de ces propriétés est color. Lorsque vous déclarez cette valeur à l'intérieur d'un champ d'application de donut, l'élément color hérite toujours des enfants inclus 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 de rustam burkhanov sur Unsplash)