Natives CSS-Scoping mit @scope
Native CSS @scope begrenzt Selektoren auf eine Komponente oder Donut-Grenze, mit Nähe im Cascade und ohne Build-Schritt.
@scope ist eine CSS-At-Rule, die einschränkt, wo ein Block von Selektoren greifen kann – von einem Wurzelelement bis hin zu einer optionalen unteren Grenze, ohne Build-Schritt und ohne zusätzliche Spezifität. MDN führt sie seit März 2026 mit dem Baseline-Status „newly available”.
Wer eine Komponenten-Codebasis pflegt, bezahlt bereits an irgendeiner Stelle für Style-Containment: mit einer Namenskonvention, die im Code-Review durchgesetzt wird, mit einem Bundler-Plugin, das Klassennamen hasht, oder mit einer Runtime, die Style-Tags injiziert. All das existiert, weil ein einfacher Nachfahren-Selektor zu weit reicht und ein verketteter Kind-Selektor das CSS an eine exakte DOM-Struktur schweißt.
Wichtigste Erkenntnisse
- Innerhalb eines
@scope-Blocks behält ein nackter Selektor nur seine eigene Spezifität, denn das implizierte Präfix lautet:where(:scope)und:where()trägt kein Gewicht bei; schreibt man:scopeexplizit, kommen 0-1-0 hinzu. @scope (.card) to (.card__content)trifft Elemente zwischen der Card und ihrem Content-Slot: Die Wurzel ist eingeschlossen, das Limit-Element und alles darunter sind ausgeschlossen.- Wenn zwei gescopte Deklarationen bei der Spezifität gleichauf liegen, gewinnt diejenige, deren Scope-Wurzel weniger DOM-Schritte vom Element entfernt ist – unabhängig von der Quellreihenfolge.
- Die Scoping-Nähe (Proximity) wird nach Wichtigkeit, Cascade Layers und Spezifität, aber vor der Quellreihenfolge verglichen. Ein ungescopter Selektor mit höherer Spezifität überschreibt eine gescopte Regel also weiterhin.
@scopebegrenzt, wo Selektoren greifen; es verhindert nicht, dass vererbte Eigenschaften wiecolorüber die Scope-Grenze hinaus in den ausgeschlossenen Bereich fließen.
Warum lecken Selektoren, und was leisten BEM, CSS Modules und CSS-in-JS dagegen?
Jeder CSS-Selektor wird gegen das gesamte Dokument geprüft. Wer also „das Hero-Bild in dieser Card” ansprechen will, muss sich zwischen einem zu strukturabhängigen und einem zu breiten Selektor entscheiden.
.card > .card__body > img { } /* 0-2-1, breaks when the markup moves */
img { } /* 0-0-1, matches every image on the page */
.card__img { } /* 0-1-0, BEM: a unique name per component */
BEM löst das Reichweitenproblem mit Namensdisziplin: ein eindeutiger Block-Name, und jedes innere Element trägt eine block__element-Klasse, sodass ein nacktes .title gar nicht erst existiert. CSS Modules automatisieren dieselbe Idee, indem sie jeden Klassennamen zur Build-Zeit in einen gehashten, dateilokalen Bezeichner umschreiben. CSS-in-JS-Bibliotheken erledigen das zur Laufzeit oder zur Compile-Zeit, indem sie diese Bezeichner aus dem Komponentencode generieren; der aktuelle Stand dieses Ökosystems wird an anderer Stelle behandelt. Der Entwurf zu CSS Cascade Level 6 beschreibt, wie diese Werkzeuge intern arbeiten: Sie markieren jedes Element einer Komponente mit einem Marker-Attribut oder einer Marker-Klasse und hängen diesen Marker anschließend an jeden Selektor der Datei.
| Ansatz | Build-Schritt | Untere Grenze (Donut) | Proximity in der Kaskade | Zusätzliche Spezifität |
|---|---|---|---|---|
| BEM | Nein | Nur per Namenskonvention | Nein | Klassen-Niveau pro Name |
| CSS Modules | Ja | Gehashte Klassen pro Datei | Nein | Klassen-Niveau pro Name |
| CSS-in-JS | Laufzeit oder Compile-Zeit | Generierte Klasse pro Komponente | Nein | Klassen-Niveau pro Name |
@scope | Nein | Native to (limit)-Klausel | Ja | Keine durch die Wurzel |
Der einfache CSS-Scope-Block: @scope (.card)
@scope (.card) { img { } } trifft nur <img>-Elemente, die inklusive Nachfahren einer .card sind, und die .card-Wurzel trägt nichts zur Spezifität von img bei.
<article class="card">
<img src="hero.jpg" alt=""> <!-- matched -->
</article>
<img src="logo.svg" alt=""> <!-- not matched -->
@scope (.card) {
img { border-radius: 8px; } /* specificity 0-0-1 */
:scope { padding: 1rem; } /* specificity 0-1-0, the .card itself */
}
MDNs Hinweise zur Spezifität innerhalb eines Scopes erklären den Mechanismus. Ein nackter Selektor im Block wird so ausgewertet, als stünde das Präfix :where(:scope) davor, und da :where() selbst kein Gewicht besitzt, steuert die Wurzel nichts zur Gesamtspezifität bei. Das ist das genaue Gegenteil dessen, was Werkzeuge mit Attribut-Stempelung tun: Ein generierter [data-v-abc123]-Hook fügt jedem betroffenen Selektor Gewicht auf Attribut-Niveau hinzu. Explizit geschrieben ist :scope eine ganz normale Pseudoklasse und steuert 0-1-0 bei, :scope img liegt also bei 0-1-1.
Was ist ein Donut-Scope?
Ein Donut-Scope, geschrieben als @scope (.card) to (.card__content), stylt alles von der Card abwärts bis ausschließlich .card__content und dessen Teilbaum – genau das Slotted-Content-Problem, das verschachtelte Komponenten erzeugen.
<article class="card">
<img src="hero.jpg" alt=""> <!-- in scope -->
<div class="card__content">
<img src="inline.jpg" alt=""> <!-- excluded: below the limit -->
</div>
</article>
@scope (.card) to (.card__content) {
img { border: 4px solid goldenrod; }
}
Standardmäßig zählt die Wurzel selbst als im Scope liegend, das Limit-Element hingegen nicht. Hängt man > * an einen der beiden Selektoren an, verschiebt sich diese Grenze. @scope (.card) to (.card__content > *) holt das .card__content-Element selbst in den Scope, schließt aber dessen Kinder weiterhin aus – nützlich, wenn der Slot-Wrapper Padding braucht, der eingesetzte Inhalt aber unangetastet bleiben soll. Nach der Definition im Entwurf qualifiziert sich ein Element, wenn es auf oder unterhalb der Wurzel liegt und weder selbst ein Limit ist noch unterhalb eines Limits sitzt. Keine Selektor-Kombination drückt „Nachfahre von X, aber nicht innerhalb von Y” aus, ohne entweder eine Grenzklasse an jedem Element oder :not()-Ketten zu erfordern, die wiederum Spezifität einführen.
Scoping-Proximity schlägt die Quellreihenfolge
Wenn zwei gescopte Regeln bei der Spezifität gleichauf liegen, gewinnt die Deklaration, deren Scope-Wurzel dem Element am nächsten liegt. Das behebt den Nested-Theme-Bug, den einfache Nachfahren-Selektoren falsch handhaben.
<div class="theme-light">
<p>Light</p>
<div class="theme-dark">
<p>Dark</p>
<div class="theme-light">
<p>Light again?</p>
</div>
</div>
</div>
Mit gewöhnlichen Selektoren trifft der innerste Absatz sowohl .theme-light p als auch .theme-dark p mit jeweils 0-1-1. Es gewinnt also die Regel, die im Stylesheet zuletzt steht, und der Absatz wird in der Farbe des Dark-Themes dargestellt, obwohl er in einem hellen Container sitzt.
@scope (.theme-light) {
p { color: #1b1b1b; }
}
@scope (.theme-dark) {
p { color: #f2f2f2; } /* declared later, but loses on the inner p */
}
Das innerste <p> ist einen Schritt von seiner .theme-light-Wurzel entfernt und zwei von .theme-dark, also greift die helle Regel. MDN geht dasselbe Beispiel Schritt für Schritt durch, und nach der Proximity-Regel der Spezifikation kann eine Regel ohne Scoping-Wurzel diesen Wettbewerb niemals gewinnen, da ihre Schrittzahl als unendlich gilt.
Wo steht Proximity in der Kaskade?
Die Scoping-Proximity wird nach Wichtigkeit, Cascade Layers und Spezifität sowie vor der Quellreihenfolge verglichen. Ein ungescopter Selektor mit höherer Spezifität überschreibt eine gescopte Regel daher weiterhin, egal wie nah die Scope-Wurzel liegt.
Die Sortierreihenfolge aus Cascade 6 nennt sieben Kriterien in absteigender Priorität:
- Ursprung (Origin) und Wichtigkeit
- Kontext (Shadow-Tree-Kapselung)
- Das style-Attribut
- Cascade Layers
- Spezifität
- Scope-Proximity
- Reihenfolge des Auftretens
Proximity ist ein Tiebreaker für die Spezifität, kein Ersatz dafür:
@scope (aside) {
p { color: green; } /* 0-0-1, scoped */
}
aside#sidebar p { color: red; } /* 1-0-2, unscoped, wins */
<aside id="sidebar"><p>This is red.</p></aside>
Proximity unterhalb der Spezifität einzuordnen, war eine bewusste Entscheidung; das Änderungsprotokoll des Entwurfs dokumentiert die Streichung der stärkeren Variante, die sie überstimmt hätte. Die Begründung findet sich im Explainer zum Feature: Würde Proximity zuerst gewinnen, entschiede die Spezifität nur noch Konflikte zwischen Selektoren gleicher Nähe – Regeln, die bewusst über- oder untereinander geschrieben wurden, würden dann abhängig von der DOM-Struktur gewinnen oder verlieren. Wer erwartet, dass sich gescopte Styles wie eine Shadow-DOM-Kapselung verhalten, stößt genau hier an die Grenze dieser Erwartung.
Selektoren sind gescopt, Vererbung nicht
@scope begrenzt, wo ein Selektor greifen kann; es verhindert nicht, dass vererbte Eigenschaften über die Scope-Grenze hinausfließen. Ein auf der Scope-Wurzel gesetztes color erreicht also weiterhin jedes Element im ausgeschlossenen Bereich.
<article class="card">
<p>Card text</p>
<div class="card__content">
<p>Slotted text: also hotpink, with no border</p>
</div>
</article>
@scope (.card) to (.card__content) {
:scope { color: hotpink; } /* inherited: crosses the limit */
p { border: 1px solid currentColor; } /* not inherited, and p in the slot is out of scope */
}
Der eingesetzte Absatz liegt außerhalb des Scopes, kein gescopter Selektor trifft ihn, und er erhält daher keinen Rahmen. Trotzdem wird er in Hotpink gerendert, denn Vererbung ist ein Mechanismus auf Eigenschaftsebene, der nach der Kaskade läuft und nichts von Scopes weiß. Die @scope-Referenz formuliert denselben Punkt: Scoping grenzt ein, welche Elemente ein Selektor erreichen kann – nicht, wo die resultierenden Styles am Ende landen. Alles, was an der Slot-Grenze eingedämmt werden soll, braucht einen expliziten Reset am Slot selbst, genau wie bisher.
Welche Browser unterstützen @scope, und wann sollte man es einsetzen?
MDN stuft @scope seit März 2026 als Baseline „newly available” ein, das heißt: Jede aktuelle große Engine liefert es aus. Die Kompatibilitätsdaten zeigen erste Unterstützung in Chrome und Edge 118 sowie Firefox 146; Safari 17.4 hat es ausgeliefert, Safari 26.0 bis 26.3 sind als teilweise unterstützend markiert, und Safari 26.4 hat die volle Unterstützung wiederhergestellt. Browser ohne Unterstützung verwerfen die gesamte At-Rule, wie CSS es für unbekannte Konstrukte vorschreibt. Ein gescopter Block degradiert also zu nichts statt zu einer kaputten Regel.
Greifen Sie zu @scope, wenn eine Komponente einen Bereich des Baums besitzt, aber nicht stylen darf, was in sie hineingereicht wird, oder wenn dieselbe Komponente mit unterschiedlichen Varianten in sich selbst verschachtelt wird. Lassen Sie es bei globalen Resets, Typografie und Brand-Tokens weg: Diese sollen durchfließen, und die Kaskade ist genau dafür gebaut. Wenn Sie ganze Stylesheets gegeneinander ordnen müssen, statt Teilbäume abzugrenzen, bleiben Cascade Layers das richtige Werkzeug.
@scope verlagert Containment aus der Build-Pipeline in den Browser: Donut-Grenzen und Proximity-Auflösung sind jetzt Features der Kaskade, keine Konventionen oder generierten Hashes mehr. Der praktische nächste Schritt: Nehmen Sie eine Komponente, die derzeit auf ein __element-Suffix oder eine gehashte Klasse angewiesen ist, um ihren inneren Slot unangetastet zu lassen, schreiben Sie sie als @scope (.component) to (.slot)-Block um und prüfen Sie, ob jede Farbe oder Schrift, die am Slot enden sollte, dort explizit zurückgesetzt wird.
FAQs
Kann ich @scope per @supports feature-detecten, und was passiert in Browsern ohne Unterstützung?
Browser ohne @scope-Unterstützung verwerfen den At-Rule-Block, die gescopten Regeln greifen also nicht und sonst bricht nichts. CSS Conditional Rules Level 5 definiert @supports at-rule(@scope), aber at-rule() ist neuer als @scope (Chromium 148 hat es zuerst ausgeliefert). Jeder Browser, der alt genug ist, um @scope nicht zu kennen, kennt also auch die Detektionsfunktion nicht. Schreiben Sie schlichte Fallback-Regeln gleicher oder geringerer Spezifität außerhalb des Blocks; unterstützende Browser überschreiben sie mit den gescopten Regeln.
Kann ich @scope ohne Wurzelselektor verwenden?
Ja. Innerhalb eines HTML-style-Elements können Sie @scope ohne Wurzelselektor im Prelude schreiben; der Browser scopt die enthaltenen Regeln dann auf das Elternelement dieses style-Elements. Die Inline-Form akzeptiert auch ein Limit, geschrieben als @scope to (.card__content). Das eignet sich für serverseitig gerenderte Fragmente, die ihre eigenen Styles mitliefern. In gewöhnlichen Stylesheets sollten Sie die Prelude-Form (root) verwenden, damit die Scope-Wurzel explizit ist.
Kann ich @scope-Regeln aus JavaScript auslesen?
Ja, über das Interface CSSScopeRule, das CSSGroupingRule erweitert. Es stellt zwei schreibgeschützte String-Eigenschaften bereit: start liefert den serialisierten Selektor der Scope-Wurzel und end den Selektor des Scope-Limits; beide sind null, wenn der jeweilige Teil des Preludes fehlt. Eine Regel erreichen Sie über document.styleSheets und dessen cssRules-Liste; auf die darin enthaltenen gescopten Style-Regeln greifen Sie über die von CSSGroupingRule geerbte Eigenschaft cssRules zu.
Was ist der Unterschied zwischen @scope und Shadow DOM bei der Style-Kapselung?
Shadow DOM erzeugt einen eigenen DOM-Baum mit einer harten Style-Grenze: Äußere Selektoren können außer über ::part nichts innerhalb einer Shadow Root treffen, und innere Regeln reichen nicht nach außen. @scope ändert nichts am Markup; es begrenzt lediglich, wo die Selektoren innerhalb eines Blocks greifen können, sodass andere Stylesheets weiterhin jedes Element im Scope ansprechen können. Vererbte Eigenschaften überschreiten beide Grenzen. @scope benötigt zudem weder JavaScript noch eine Shadow Root.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k