Begrenzen Sie die Reichweite Ihrer Selectors mit der CSS-„@scope at-rule“

Hier erfahren Sie, wie Sie mit @scope Elemente nur in einem eingeschränkten Teilbaum Ihres DOM auswählen.

Veröffentlicht: 4. Oktober 2023

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

Beim Schreiben von Selektoren kann es vorkommen, dass Sie sich zwischen zwei Welten hin- und hergerissen fühlen. Einerseits sollten Sie sehr genau angeben, welche Elemente Sie auswählen. Andererseits sollten Ihre Selektoren leicht zu überschreiben sein und nicht eng an die DOM-Struktur gekoppelt sein.

Wenn Sie beispielsweise das Hero-Bild im Inhaltsbereich der Kartenkomponente auswählen möchten, was eine ziemlich spezifische Elementauswahl ist, sollten Sie wahrscheinlich keinen Selektor wie .card > .content > img.hero schreiben.

  • Dieser Selektor hat eine ziemlich hohe Spezifität von (0,3,1), was es schwierig macht, ihn zu überschreiben, wenn Ihr Code wächst.
  • Da der Kombinator für direkte untergeordnete Elemente verwendet wird, ist er eng an die DOM-Struktur gekoppelt. Sollte sich das Markup einmal ändern, müssen Sie auch Ihr CSS ändern.

Sie sollten aber auch nicht nur img als Selektor für dieses Element verwenden, da dadurch alle Bildelemente auf Ihrer Seite ausgewählt würden.

Die richtige Balance zu finden, ist oft eine Herausforderung. Im Laufe der Jahre haben einige Entwickler Lösungen und Workarounds entwickelt, die Ihnen in solchen Situationen helfen können. Beispiel:

  • Methoden wie BEM schreiben vor, dass Sie diesem Element die Klasse card__img card__img--hero zuweisen, um die Spezifität niedrig zu halten und gleichzeitig genau festlegen zu können, was Sie auswählen.
  • Bei JavaScript-basierten Lösungen wie Scoped CSS oder Styled Components werden alle Selektoren neu geschrieben, indem zufällig generierte Strings wie sc-596d7e0e-4 hinzugefügt werden. So wird verhindert, dass sie auf Elemente auf der anderen Seite Ihrer Seite ausgerichtet sind.
  • In einigen Bibliotheken werden Selektoren sogar ganz abgeschafft und Sie müssen die Styling-Trigger direkt in das Markup einfügen.

Was aber, wenn Sie keine dieser Funktionen benötigen? Was wäre, wenn Sie mit CSS genau festlegen könnten, welche Elemente ausgewählt werden sollen, ohne dass Sie Selektoren mit hoher Spezifität oder Selektoren schreiben müssten, die eng mit Ihrem DOM verknüpft sind? Hier kommt @scope ins Spiel. Damit können Sie Elemente nur in einem Unterbaum Ihres DOM auswählen.

Einführung von @scope

Mit @scope können Sie die Reichweite Ihrer Selektoren einschränken. Dazu legen Sie den Bereichs-Root fest, der die obere Grenze des Unterbaums bestimmt, auf den Sie abzielen möchten. Wenn ein Bereichs-Root festgelegt ist, können die enthaltenen Stilregeln, die als bereichsbezogene Stilregeln bezeichnet werden, nur aus diesem eingeschränkten Unterbaum des DOM ausgewählt werden.

Wenn Sie beispielsweise nur die <img>-Elemente in der Komponente .card ausrichten möchten, legen Sie .card als Bereichsstamm der @-Regel @scope fest.

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

Mit der bereichsbezogenen Stilregel img { … } können effektiv nur <img>-Elemente ausgewählt werden, die im Bereich des übereinstimmenden .card-Elements liegen.

Damit die <img>-Elemente im Inhaltsbereich der Karte (.card__content) nicht ausgewählt werden, können Sie den img-Selektor genauer definieren. Eine weitere Möglichkeit ist, die @scope-Regel zu verwenden, die auch ein Bereichslimit akzeptiert, das die Untergrenze bestimmt.

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

Diese bereichsbezogene Stilregel gilt nur für <img>-Elemente, die im Ancestor-Baum zwischen .card- und .card__content-Elementen platziert sind. Diese Art der Bereichsabgrenzung mit einer Ober- und einer Untergrenze wird oft als Donut-Bereich bezeichnet.

Der :scope-Selektor

Standardmäßig sind alle bereichsbezogenen Stilregeln relativ zum Bereichs-Root. Es ist auch möglich, das Scoping-Stammelement selbst auszurichten. Verwenden Sie dazu die Auswahl :scope.

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

Selektoren in Stilregeln mit Bereich erhalten implizit :scope vorangestellt. Wenn Sie möchten, können Sie das auch explizit angeben, indem Sie :scope voranstellen. Alternativ können Sie den Selektor & aus CSS-Nesting voranstellen.

@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 */
    }
}

Für eine Bereichseinschränkung kann die Pseudoklasse :scope verwendet werden, um eine bestimmte Beziehung zum Bereichsstamm zu erzwingen:

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

Ein Bereichsgrenzwert kann auch auf Elemente außerhalb des Bereichsstammelements verweisen, indem :scope verwendet wird. Beispiel:

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

Die bereichsbezogenen Stilregeln selbst können den Teilbaum nicht verlassen. Auswahlen wie :scope + p sind ungültig, da damit versucht wird, Elemente auszuwählen, die nicht im Bereich sind.

@scope und Spezifität

Die Selektoren, die Sie im Prelude für @scope verwenden, haben keine Auswirkungen auf die Spezifität der enthaltenen Selektoren. In unserem Beispiel ist die Spezifität des Selektors img weiterhin (0,0,1).

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

Die Spezifität von :scope entspricht der einer regulären Pseudoklasse, nämlich (0,1,0).

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

Im folgenden Beispiel wird & intern in den Selektor umgeschrieben, der für den Scoping-Stamm verwendet wird, und in einen :is()-Selektor eingeschlossen. Am Ende verwendet der Browser :is(#sidebar, .card) img als Selektor für den Abgleich. Dieser Vorgang wird als Desugaring bezeichnet.

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

Da & mit :is() desugariert wird, wird die Spezifität von & gemäß den Spezifitätsregeln für :is() berechnet: Die Spezifität von & entspricht der Spezifität des spezifischsten Arguments.

Angewendet auf dieses Beispiel entspricht die Spezifität von :is(#sidebar, .card) der des spezifischsten Arguments, nämlich #sidebar, und wird daher zu (1,0,0). In Kombination mit der Spezifität von img, die (0,0,1) ist, ergibt sich für den gesamten komplexen Selektor eine Spezifität von (1,0,1).

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

Der Unterschied zwischen :scope und & in @scope

Neben den Unterschieden bei der Berechnung der Spezifität gibt es einen weiteren Unterschied zwischen :scope und &: :scope steht für den abgeglichenen Bereichsstamm, während & für den Selektor steht, der zum Abgleichen des Bereichsstamms verwendet wird.

Aus diesem Grund kann & mehrmals verwendet werden. Das steht im Gegensatz zu :scope, das Sie nur einmal verwenden können, da Sie keine Bereichsroot innerhalb einer Bereichsroot abgleichen können.

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

Bereich ohne Prelude

Wenn Sie Inlinestyles mit dem <style>-Element schreiben, können Sie die Styleregeln auf das umschließende übergeordnete Element des <style>-Elements beschränken, indem Sie keinen Scoping-Root angeben. Dazu lassen Sie die Einleitung von @scope weg.

<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>

Im obigen Beispiel beziehen sich die Regeln mit Bereich nur auf Elemente innerhalb des div mit dem Klassennamen card__header, da dieses div das übergeordnete Element des <style>-Elements ist.

@scope in der Kaskade

Innerhalb der CSS-Kaskade wird mit @scope auch ein neues Kriterium hinzugefügt: Scoping-Nähe. Dieser Schritt erfolgt nach der Spezifität, aber vor der Reihenfolge der Darstellung.

Visualisierung der CSS-Kaskade.

Gemäß Spezifikation:

Beim Vergleich von Deklarationen, die in Stilregeln mit unterschiedlichen Scoping-Roots vorkommen, gewinnt die Deklaration mit den wenigsten Generationen- oder Geschwister-Element-Hops zwischen der Scoping-Root und dem Subjekt der Stilregel mit Bereich.

Dieser neue Schritt ist praktisch, wenn Sie mehrere Varianten einer Komponente verschachteln. Hier ist ein Beispiel, in dem @scope noch nicht verwendet wird:

<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>

Wenn Sie sich diesen kleinen Markup-Ausschnitt ansehen, ist der dritte Link white anstelle von black, obwohl er ein untergeordnetes Element eines div mit der angewendeten Klasse .light ist. Das liegt am Kriterium der Reihenfolge des Erscheinens, das in der Kaskade verwendet wird, um den Gewinner zu ermitteln. Da .dark a zuletzt deklariert wurde, gewinnt es gemäß der Regel .light a.

Mit dem Kriterium für die räumliche Nähe wird dieses Problem nun behoben:

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

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

Da beide bereichsbezogenen a-Selektoren dieselbe Spezifität haben, wird das Kriterium für die Nähe des Bereichs angewendet. Dabei wird die Nähe der beiden Selektoren zur Scoping-Root berücksichtigt. Für das dritte a-Element ist es nur ein Hop zum .light-Bereichsstamm, aber zwei zum .dark-Bereichsstamm. Daher gewinnt der Selektor a in .light.

Selektorisolation, nicht Stilisolation

@scope schränkt die Reichweite der Selektoren ein. Es bietet keine Stilisolation. Attribute, die an untergeordnete Elemente weitergegeben werden, werden auch über die Untergrenze von @scope hinaus weitergegeben. Eine solche Property ist color. Wenn Sie eine solche Variable in einem Donut-Bereich deklarieren, wird sie weiterhin an untergeordnete Elemente im Loch des Donuts vererbt.color

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

Im Beispiel haben das .card__content-Element und seine untergeordneten Elemente die Farbe hotpink, da sie den Wert von .card übernehmen.