mitmario.dev

Stile begrenzen mit @scope

CSS Im Browser 4 Min Lesezeit 3 BeispieleLektion 4 von 8

In jeder gewachsenen Seite färbt irgendwo .karte a { color: blau } die Links der Karte. Und dann steckt jemand ein fremdes Widget in die Karte, einen eingebetteten Kalender oder eine Kommentarliste. Prompt werden deren Links auch blau.

Bisher gab es dagegen zwei Auswege, beide unschön. Entweder ein längerer Selektor mit :not(), der jede neue Ausnahme mitschleppt, oder eine Namenskonvention, die alle Beteiligten kennen und einhalten müssen. Mit @scope bekommt die Regel eine Untergrenze.

Anfang und Ende

Der Bereich mit einem Loch darin
body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

a {
  color: #18181b;
}

.karte {
  max-width: 420px;
  padding: 24px;
  border: 2px solid #d4d4d8;
  border-radius: 12px;
}

.eingebettet {
  padding: 16px;
  border-radius: 8px;
  background-color: #f4f4f5;
}

/* Ab .karte nach unten, aber nicht weiter als bis
   .eingebettet. Das Widget bleibt außen vor. */
@scope (.karte) to (.eingebettet) {
  :scope {
    border-color: #2563eb;
  }

  a {
    color: #2563eb;
    font-weight: 600;
  }
}

@scope (.karte) { ... } heißt, dass alles darin ab jeder .karte nach unten gilt. Das allein ist noch nichts Besonderes. Es entspricht ungefähr einem vorangestellten .karte.

Interessant wird der zweite Teil. @scope (.karte) to (.eingebettet) schneidet ein Loch heraus. Der Bereich beginnt bei .karte und endet vor jedem .eingebettet. Alles dazwischen ist drin, alles darunter ist draußen. Man nennt das Donut-Scoping, und der Name trifft es genau.

Dieselbe Absicht ohne @scope
body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

a {
  color: #18181b;
}

.karte {
  max-width: 420px;
  padding: 24px;
  border: 2px solid #d4d4d8;
  border-radius: 12px;
}

.eingebettet {
  padding: 16px;
  border-radius: 8px;
  background-color: #f4f4f5;
}

/* Der Selektor sagt nur, wo es anfängt. Er trifft
   deshalb auch den Link im fremden Widget. */
.karte a {
  color: #2563eb;
  font-weight: 600;
}

Die beiden Beispiele haben dasselbe Markup und dasselbe Ziel, einmal mit Bereich und einmal ohne. Im zweiten färbt sich der Link im fremden Widget mit, denn ein Selektor kann eben nur sagen, wo etwas anfängt.

Im Auskunftsstreifen ist der Unterschied eine einzige Zeile. Im ersten Beispiel steht am Link in der Karte color: rgb(37, 99, 235) und am Link im eingebetteten Widget color: rgb(24, 24, 27), also die Farbe aus der Basisregel. Im zweiten Beispiel stehen an beiden Links rgb(37, 99, 235). Das Loch im Donut ist damit ein Wert, der sich unterscheidet, und den findest du im Reiter „Werte“.

Im Reiter „Regeln“ fehlt @scope dagegen noch. Dort steht am Link im Widget die Bereichsregel als gewinnend, obwohl rgb(24, 24, 27) gilt.

:scope ist die Wurzel des Bereichs

Innerhalb eines @scope-Blocks verweist :scope auf das Element, an dem der Bereich beginnt, im Beispiel also auf die Karte selbst. So gestaltest du die Komponente und ihr Inneres im selben Block, ohne den Klassennamen zu wiederholen.

Selektoren im Block gelten automatisch relativ zur Wurzel. Ein a darin meint also jeden Link innerhalb der Wurzel, nicht “jeder Link im Dokument”. Anders als beim Verschachteln in Lektion 15.2 bringt die Wurzel dabei aber keine Spezifität mit. Das a bleibt bei 0-0-1, als stünde dort :where(:scope) a. Verschachtelt in .karte wäre es 0-1-1. Soll die Spezifität der Wurzel mitzählen, schreibst du :scope a.

Die Kaskade bekommt eine Stufe dazu

Nähe schlägt Reihenfolge
body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

.hell {
  padding: 16px;
  background-color: #f4f4f5;
}

.dunkel {
  padding: 16px;
  background-color: #18181b;
  color: #fafafa;
}

/* Beide Regeln haben dieselbe Spezifität, und die
   für den hellen Bereich steht sogar weiter unten.
   Der Link im dunklen Kasten wird trotzdem hell,
   weil dessen Bereichswurzel näher an ihm liegt. */
@scope (.dunkel) {
  a {
    color: #93c5fd;
  }
}

@scope (.hell) {
  a {
    color: #1d4ed8;
  }
}

Das hier ist wirklich neu. In Abschnitt 3 waren es vier Stufen, nämlich Herkunft, Layer, Spezifität und Reihenfolge. Jetzt schiebt sich eine fünfte dazwischen, die Nähe, und zwar zwischen Spezifität und Reihenfolge.

Haben zwei Regeln dieselbe Spezifität und stehen beide in einem Bereich, gewinnt die, deren Bereichswurzel näher am Element liegt. Im dritten Beispiel steht die Regel für den hellen Bereich weiter unten. Trotzdem gewinnt im dunklen Kasten die Regel des dunklen Bereichs, denn ihre Wurzel ist einen Schritt näher.

Nachlesen kannst du das am Ergebnis. Der äußere Link steht auf color: rgb(29, 78, 216), der innere auf rgb(147, 197, 253). Welche Regel dabei verloren hat, steht allerdings nicht im Streifen. Dort steht immer nur, was gilt. Für die Frage, warum eine Regel gewonnen hat, bleiben dir der eigene Kopf oder die Werkzeuge des Browsers.

Am Ende entscheidet also die innerste Umgebung. Verschachtelte Themen, Dunkelmodus-Inseln und eingebettete Komponenten funktionieren damit ohne einen einzigen zusätzlichen Klassennamen.

Womit du rechnen musst

Ein Bereich ist keine Kapsel. Was von außen kommt, kommt weiterhin an, Vererbung eingeschlossen, und eine Regel mit höherer Spezifität von außen gewinnt nach wie vor. @scope schützt nicht vor der Kaskade. Es begrenzt nur die Reichweite deiner eigenen Regeln.

Wie alles in diesem Abschnitt ist es noch nicht so alt, dass ich es ohne kurzes Nachsehen hinschreibe. Alle drei großen Browser können @scope inzwischen, ein älteres Gerät womöglich nicht.

Mit @supports kommst du hier allerdings nicht weiter, und das ist eine lehrreiche Ausnahme. @supports selector(:scope) sieht nach der passenden Frage aus, ist aber keine. :scope gibt es nämlich seit 2020 überall, auch auf oberster Ebene und in querySelector, völlig unabhängig von @scope. Damit ist die Frage in jedem Browser mit Ja beantwortet, auch in einem, der @scope nicht kennt.

@supports fragte lange überhaupt nur nach Eigenschaften und Selektoren. Seit Chrome 148 und Firefox 157 vom 29.09.2026 kennt es zusätzlich at-rule(@scope), Safari noch nicht (Stand Oktober 2026). Ein Browser, der die Frage nicht versteht, beantwortet sie mit Nein. Ausgerechnet Safari sagt also Nein, obwohl es @scope seit Version 26.4 kann.

Seit dem 24.03.2026 kennen alle großen Browser @scope. Muss die Reichweite für ältere trotzdem abgesichert sein, bleibt der alte Weg, erst die Fassung mit einem längeren Selektor und danach die mit @scope obendrauf.

In der Aufgabe hat eine Karte drei Links, und einer davon gehört jemand anderem.

Zum Mitnehmen

Ein Selektor sagt, wo eine Regel anfängt, aber nicht, wo sie aufhört. @scope gibt einer Regel beides, Anfang und Ende, und bei gleicher Spezifität gewinnt die nähere Wurzel.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.