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

Hier erfahren Sie, wie Sie mit @scope Elemente nur innerhalb eines begrenzten Teilbaums Ihres DOM auswählen.

Unterstützte Browser

  • Chrome: 118.
  • Edge: 118.
  • Firefox: hinter einer Flagge.
  • Safari: 17.4.

Quelle

Die schwierige Kunst, CSS-Selektoren zu schreiben

Wenn Sie Selektoren schreiben, werden Sie möglicherweise zwischen zwei Welten hin und her gerissen. Einerseits sollten Sie genau festlegen, welche Elemente Sie auswählen. Andererseits sollten Ihre Selectors einfach zu überschreiben sein und nicht eng mit der DOM-Struktur verbunden sein.

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

  • Dieser Selektor hat eine ziemlich hohe Spezifizität von (0,3,1), was es schwierig macht, ihn zu überschreiben, wenn der Code wächst.
  • Da er auf dem Direktuntergeordneten-Kombinator basiert, ist er eng mit der DOM-Struktur verknüpft. Sollte sich das Markup ändern, müssen Sie auch das CSS ändern.

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

Dabei ist es oft gar nicht so einfach, die richtige Balance zu finden. Im Laufe der Jahre haben einige Entwickler Lösungen und Behelfslösungen entwickelt, die euch in solchen Situationen helfen können. Beispiel:

  • Bei Methoden wie BEM wird empfohlen, diesem Element die Klasse card__img card__img--hero zuzuweisen, um die Spezifität niedrig zu halten und gleichzeitig die Auswahl präzise zu gestalten.
  • JavaScript-basierte Lösungen wie begrenztes CSS oder Styled Components überschreiben alle Ihre Selektoren, indem sie Ihren Selektoren zufällig generierte Strings wie sc-596d7e0e-4 hinzufügen, um zu verhindern, dass sie auf Elemente auf der anderen Seite Ihrer Seite ausgerichtet werden.
  • Einige Bibliotheken haben Selectoren sogar ganz abgeschafft und erfordern, dass Sie die Stilauslöser direkt in das Markup einfügen.

Aber was ist, wenn Sie keine davon brauchen? Was wäre, wenn Sie mit CSS sehr spezifisch angeben könnten, welche Elemente Sie auswählen, ohne dass Sie Selektoren mit hoher Spezifität oder eng mit Ihrem DOM gekoppelte Selektoren schreiben müssen? Hier kommt @scope ins Spiel. Damit können Sie Elemente nur innerhalb eines untergeordneten Knotens Ihres DOM auswählen.

Jetzt neu: @scope

Mit @scope können Sie die Reichweite Ihrer Auswahlkriterien einschränken. Dazu legen Sie den Begrenzungsknoten fest, der die Obergrenze des untergeordneten Knotens bestimmt, auf den Sie Ihre Anzeigen ausrichten möchten. Wenn ein Wurzelknoten festgelegt ist, können die enthaltenen Stilregeln, die sogenannten Stilregeln mit Bereich, nur aus diesem begrenzten Teilbaum des DOM ausgewählt werden.

Wenn Sie beispielsweise nur ein Targeting auf die <img>-Elemente in der Komponente „.card“ vornehmen möchten, legen Sie .card als Geltungsbereich der At-Regel für @scope fest.

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

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

Wenn Sie verhindern möchten, dass die <img>-Elemente im Inhaltsbereich der Karte (.card__content) ausgewählt werden, können Sie den img-Selektor präzisieren. Eine andere Möglichkeit besteht darin, dass die At-rule @scope auch ein Begrenzungslimit akzeptiert, das die Untergrenze bestimmt.

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

Diese auf einen Bereich reduzierte Stilregel zielt nur auf <img>-Elemente ab, die sich zwischen .card- und .card__content-Elementen in der Ancestor-Struktur befinden. Diese Art der Begrenzung mit Ober- und Untergrenze wird oft als Donut-Begrenzung bezeichnet.

Der :scope-Selektor

Standardmäßig beziehen sich alle stilistischen Regeln mit Bereich auf den Stamm des Bereichs. Es ist auch möglich, das Stammelement des Gültigkeitsbereichs selbst als Ziel auszuwählen. 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 stilspezifischen Regeln werden implizit mit :scope vorangestellt. Wenn Sie möchten, können Sie dies auch explizit angeben, indem Sie :scope voranstellen. Alternativ können Sie den &-Selektor aus CSS-Verschachtelung 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 */
    }
}

Bei einem Umfangslimit kann mit der Pseudoklasse :scope eine bestimmte Beziehung zum Umfangsstamm erforderlich sein:

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

Ein Umfangslimit kann auch auf Elemente außerhalb ihrer Gültigkeitsbasis mithilfe von :scope verweisen. Beispiel:

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

Die Stilregeln mit Bereich können den untergeordneten Knoten nicht selbst ausschließen. Auswahlen wie :scope + p sind ungültig, da damit versucht wird, Elemente auszuwählen, die nicht zum Geltungsbereich gehören.

@scope und Spezifität

Die Auswahlkriterien, die Sie im Vorspann für @scope verwenden, haben keine Auswirkungen auf die Spezifität der enthaltenen Auswahlkriterien. Im folgenden Beispiel ist die Spezifität des img-Selektors weiterhin (0,0,1).

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

:scope ist eine reguläre Pseudoklasse, nämlich (0,1,0).

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

Im folgenden Beispiel wird der & intern in den Selektor umgeschrieben, der für den Wurzelknoten des Gültigkeitsbereichs verwendet wird, und in einen :is()-Selektor eingewickelt. Der Browser verwendet dann :is(#sidebar, .card) img als Auswahl für die Übereinstimmung. Dieser Vorgang wird als Entzuckerung bezeichnet.

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

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

In diesem Beispiel ist :is(#sidebar, .card) so spezifisch wie sein spezifischstes Argument, nämlich #sidebar, und wird daher zu (1,0,0). Kombinieren Sie das mit der Spezifität von img, also (0,0,1), und Sie erhalten (1,0,1) als Spezifität für den gesamten komplexen Selektor.

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

Die Differenz zwischen :scope und & innerhalb von @scope

Neben Unterschieden bei der Berechnung der Spezifität besteht ein weiterer Unterschied zwischen :scope und & darin, dass :scope den übereinstimmenden Wurzelknoten des Gültigkeitsbereichs darstellt, während & den Selektor darstellt, der zum Abgleichen des Wurzelknotens des Gültigkeitsbereichs verwendet wird.

Daher ist es möglich, & mehrmals zu verwenden. Dies steht im Gegensatz zu :scope, das nur einmal verwendet werden kann, da ein Wurzelelement für die Begrenzung nicht innerhalb eines anderen Wurzelelements für die Begrenzung verwendet werden kann.

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

Bereich ohne Präludium

Wenn Sie Inline-Styles mit dem <style>-Element schreiben, können Sie die Stilregeln auf das einschließende übergeordnete Element des <style>-Elements anwenden, indem Sie keinen Gültigkeitsstamm angeben. Dazu lassen Sie die Vorab-Ansage von @scope aus.

<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 Beispiel oben sind die Regeln mit begrenztem Gültigkeitsbereich nur auf Elemente innerhalb des div mit dem Klassennamen card__header ausgerichtet, da div das übergeordnete Element des <style>-Elements ist.

@scope in der Kaskade

Innerhalb der CSS-Hierarchie fügt @scope auch ein neues Kriterium hinzu: Nähe zum Gültigkeitsbereich. Dieser Schritt erfolgt nach der Spezifität, aber vor der Reihenfolge der Erscheinung.

Visualisierung der CSS-Kaskade.

Gemäß der Spezifikation:

Beim Vergleichen von Deklarationen in Stilregeln mit unterschiedlichen Wurzelelementen für den Gültigkeitsbereich gilt die Deklaration mit den wenigsten Sprüngen zwischen dem Wurzelelement für den Gültigkeitsbereich und dem Subjekt der Stilregel mit Gültigkeitsbereich als Gewinner.

Dieser neue Schritt ist praktisch, wenn mehrere Varianten einer Komponente verschachtelt werden. In diesem Beispiel wird @scope noch nicht verwendet:

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

In diesem kleinen Ausschnitt des Markups ist der dritte Link white anstelle von black, obwohl er ein untergeordnetes Element eines div ist, auf das die Klasse .light angewendet wurde. Grund dafür ist die Reihenfolge der Kriterien, anhand derer die Kaskade hier den Sieger ermittelt. .dark a wurde als Letztes erklärt und gewinnt daher gegen die .light a-Regel.

Mit dem Kriterium für die Entfernung des Gültigkeitsbereichs ist dies nun gelöst:

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

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

Da beide a-Selektoren mit Bereich dieselbe Spezifität haben, wird das Näherungskriterium für den Bereich aktiviert. Dabei werden beide Selektoren nach ihrer Nähe zum Wurzelelement der Gültigkeitsebene gewichtet. Für dieses dritte a-Element ist es nur ein Sprung zum .light-Bereichsstamm, aber zwei zum .dark-Element. Daher hat der a-Selektor in .light Vorrang.

Schlussbemerkung: Auswahl-, nicht Stilisolierung

Wichtig ist, dass @scope die Reichweite der Auswahlen einschränkt, aber keine Stilisolierung bietet. Attribute, die an untergeordnete Elemente weitergegeben werden, werden auch über die Untergrenze der @scope hinaus übernommen. Eine solche Property ist color. Wenn Sie das Element innerhalb eines Donut-Bereichs deklarieren, wird color weiterhin an untergeordnete Elemente innerhalb des Lochs des Donuts vererbt.

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

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

(Coverbild von rustam burkhanov auf Unsplash)