Wer gewinnt in CSS?

Ein Element bekommt seinen finalen CSS-Wert nicht „einfach so“. CSS verarbeitet Regeln in einer festen Logik. Wenn man diese Logik einmal sauber verstanden hat, kann man fast jede „Warum greift das nicht?“-Situation systematisch erklären.

Am hilfreichsten ist es, das Ganze als CSS-Pipeline zu sehen – einen Ablauf, der bei jedem Element immer wieder durchlaufen wird:

  1. Aktivierung & Matching: Welche Regeln sind überhaupt relevant und dürfen dieses Element treffen?
  2. Kaskade: Wenn mehrere Regeln dieselbe Eigenschaft setzen: welche gewinnt?
  3. Vererbung/Defaulting: Falls niemand etwas setzt: kommt es vom Parent oder vom Initialwert?
  4. Berechnung: Ist der gewonnene Wert gültig und wie wird er ausgerechnet? (inkl. var(), Shorthands, Animationen)

Wichtig dabei: CSS entscheidet nicht „für das Element insgesamt“, sondern pro Property. Für color kann also eine ganz andere Regel gewinnen als für margin – und für font-size kann am Ende Vererbung greifen, während display aus einer expliziten Regel kommt.

Die „Kaskade“ ist dabei Schritt 2. Für das Gesamtverständnis brauchst du aber alle Schritte.


1) Aktivierung & Matching: Wer kommt überhaupt in den Kandidaten-Pool?

Bevor eine Regel gewinnen kann, muss sie überhaupt aktiv sein und matchen.

Bedingungen: @media, @supports, @container

Viele Regeln leben hinter Bedingungen:

  • @media hängt vom Viewport und von Benutzerpräferenzen ab (z. B. Dark Mode, Reduced Motion).
  • @supports hängt davon ab, ob ein Feature unterstützt wird. Nicht unterstützte Deklarationen werden ignoriert – das ist ein typisches Progressive-Enhancement-Werkzeug.
  • @container hängt von der Größe (oder manchmal auch dem Stil) eines Containers ab, nicht vom Viewport. Wichtig ist: Container Queries wirken erst dann sinnvoll, wenn du ein Element als Container definierst (z. B. mit container-type: inline-size;).

Wenn eine Bedingung nicht erfüllt ist, existieren die Deklarationen für die Kaskade gerade nicht. Das ist oft der Grund, warum man „die Regel doch sieht“, aber sie in dem Moment nicht greift.

@scope: Matching bewusst lokal halten (Zaun) – und Nähe kann gewinnen

@scope begrenzt, wo Selektoren matchen dürfen. Das ist wie ein lokaler Zaun im Light DOM: Regeln wirken nur in einem definierten Bereich.

@scope (.card) {
  h2 { margin: 0; }
}

So kannst du Komponenten-Styles kapseln, ohne alles umzubenennen oder die Spezifität hochzuschrauben.

@scope kann zusätzlich eine innere Grenze setzen („bis hierhin, aber nicht weiter“). Und: Wenn mehrere passende Regeln aus unterschiedlichen (verschachtelten) Scopes stammen, spielt Nähe eine Rolle. Der „nähere“ (innerere) Scope hat einen Vorteil gegenüber dem äußeren – genau so, wie man es bei verschachtelten Modulen erwarten würde.

Shadow DOM: harte Grenze für Selektoren

Bei Web Components mit Shadow DOM gibt es eine zusätzliche Matching-Regel:

  • CSS von außen kann den Host treffen (my-widget { … }),
  • kann aber nicht in das Innenleben (Shadow Tree) hineinselektieren.

Das ist keine Frage von Spezifität. Die Selektoren dürfen dort schlicht nicht matchen.

2) Die Kaskade: Wenn mehrere Regeln passen, wer gewinnt?

Jetzt sind wir bei der eigentlichen „Wer gewinnt?“-Maschine. Und nochmal: Sie läuft pro Property. Für color kann Regel A gewinnen, für padding Regel B, und für line-height kann am Ende Vererbung greifen.

CSS sammelt also alle passenden Deklarationen für genau diese eine Eigenschaft und sortiert sie nach Priorität. Der Gewinner liefert den sogenannten „cascaded value“.

Am leichtesten ist es, sich die Kaskade als mehrere Schichten vorzustellen. Erst wenn eine Schicht keinen eindeutigen Sieger liefert, wird die nächste Schicht relevant.

2.1 Herkunft (Origin) und !important

CSS unterscheidet, aus welcher Quelle eine Regel stammt:

  • User-Agent (Browser-Defaults),
  • User (Benutzer-Stylesheets, selten, aber im Accessibility-Umfeld relevant),
  • Author (dein CSS: Dateien, <style>, Inline).

!important hebt eine Deklaration in eine „wichtig“-Schicht. Das sorgt dafür, dass eine wichtige Deklaration fast immer normale Deklarationen schlägt. Gleichzeitig ist wichtig zu wissen: !important ist Teil dieser Herkunftslogik – darum „lebt“ es nicht völlig losgelöst von allem anderen.

2.2 Inline-Styles: style="" ist eine eigene Kraft

Inline-Styles sind Author-Styles, aber mit besonderer Priorität. Ein style="color: red" ist deshalb oft der Grund, warum eine Regel aus deiner CSS-Datei nicht gewinnt.

Für Architektur ist Inline meist unpraktisch, weil es die Pipeline schwerer nachvollziehbar macht. Für Debugging oder bewusst dynamische Werte ist es sinnvoll.

2.3 @layer: Architektur-Priorität statt Selektor-Wettrüsten

@layer ist dafür da, Prioritäten architektonisch zu definieren. Statt dass am Ende „zufällig“ eine Utility-Regel eine Komponente überschreibt (oder umgekehrt), gibst du eine klare Ordnung vor.

Typische Layer-Reihenfolge:

@layer reset, base, components, utilities, theme;

So liest sich das:

  • reset: Normalisierung
  • base: globale Typografie und Standard-Elemente
  • components: Bausteine
  • utilities: kleine Ein-Zweck-Klassen, die bewusst überschreiben dürfen
  • theme: Branding/Varianten, die bewusst am Ende liegen

Die entscheidende Wirkung: Wenn zwei Regeln in verschiedenen Layern konkurrieren, entscheidet zuerst der Layer – bevor Spezifität überhaupt wichtig wird. Das macht CSS viel planbarer.

Ein Detail, das man kennen sollte: Nicht gelayerte Regeln (unlayered) verhalten sich wie eine eigene Prioritätsstufe. Wenn du Layers nutzt, ist es meist am ruhigsten, das konsequent zu tun oder sehr bewusst zu entscheiden, was unlayered bleibt.

Außerdem gibt es revert-layer, womit du in einem späteren Layer eine Property auf den Zustand „vor diesem Layer“ zurücksetzen kannst.

2.4 @scope innerhalb der Kaskade: Nähe vor „später gewinnt“

@scope ist nicht nur ein Matching-Zaun. Bei Konflikten zwischen passenden scoped Regeln kann die Nähe (innerer Scope) den Ausschlag geben – und zwar bevor man in reines „wer steht später im CSS?“ abrutscht. Das ist besonders angenehm, wenn Module verschachtelt sind: Das innere Modul kann sich definieren, ohne gleich „stärker“ werden zu müssen.

2.5 Spezifität und Reihenfolge: erst jetzt kommt „klassisches CSS“

Wenn Herkunft/!important, Inline, Layer und Scope keine Entscheidung erzwingen, kommen die beiden Klassiker:

  • Spezifität: stärkere Selektoren gewinnen
  • Quellreihenfolge: bei Gleichstand gewinnt die spätere Regel

3) Spezifität: wie stark ist ein Selektor wirklich?

Spezifität ist ein Zählsystem. Am besten denkt man in vier Spalten:

(Inline, IDs, Klassen/Attribute/Pseudoklassen, Typen/Pseudoelemente)

  • Inline (style="") → (1,0,0,0)
  • #id → (0,1,0,0)
  • .klasse, [attr], :hover, :nth-child() → (0,0,1,0)
  • div, hello-world, ::before → (0,0,0,1)

Beispiele:

hello-world              /* (0,0,0,1) */
.card                    /* (0,0,1,0) */
.card hello-world        /* (0,0,1,1) */
#app .card hello-world   /* (0,1,1,1) */

Wichtige Klarstellungen:

  • Kombinatoren (`,>,+,~`) zählen nicht.
  • * zählt nicht.
  • „Langer Selektor“ ist nicht automatisch stark. Es zählt, was drin ist.

Komma-Listen: zwei Regeln, nicht „eine gemischte“

Ein häufiger Denkfehler:

.card h2, #page-title { color: red; }

Das ist keine einzige Regel mit „kombinierter Spezifität“, sondern eine Liste von Selektoren. Für ein Element zählt immer nur die Spezifität des Selektors, der es tatsächlich matcht. Das erklärt, warum derselbe Deklarationsblock je nach Element mal sehr „stark“ (ID), mal eher „normal“ (Klasse+Typ) wirkt.

Moderne Funktions-Pseudoklassen: :where, :is, :not, :has

Hier entstehen viele Überraschungen:

  • :where(...) hat immer Spezifität 0. Das ist ideal für Basisregeln, die du bewusst leicht überschreiben willst.
  • :is(...) nimmt die Spezifität des stärksten Arguments.
  • :not(...) verhält sich bezüglich Spezifität wie :is(...) (stärkstes Argument zählt).
  • :has(...) nimmt ebenfalls die Spezifität des stärksten Arguments und kann damit Regeln sehr schwer überschreibbar machen.

Ein typisches Muster ist deshalb:

  • Struktur/Targets mit :where(...) ausdrücken (bleibt schwach),
  • und „wirkliche Stärke“ nur dort einsetzen, wo du sie wirklich brauchst.

4) Wenn niemand etwas setzt: Vererbung und Initialwerte

Nicht jede Eigenschaft muss irgendwo explizit gesetzt sein.

  • Vererbbare Properties (z. B. color, font-*, line-height) kommen vom Eltern-Element, wenn du sie nicht setzt.
  • Nicht vererbbare Properties (z. B. margin, border, background) fallen auf ihren Initialwert zurück.

Die Schlüsselwörter helfen dir, dieses Verhalten gezielt zu steuern:

  • inherit (übernehmen),
  • initial (Initialwert),
  • unset (inherit oder initial je nach Property),
  • revert (zurück in Richtung Default/Origin),
  • revert-layer (zurück auf den Stand vor einem Layer).

Ein lehrreiches Beispiel ist line-height: eine unitless Zahl vererbt sich „flexibler“ als eine feste Länge, weil sie sich mit den Schriftgrößen der Kinder mitbewegt.

5) „Der Sieger wirkt nicht“: Berechnung, var(), Shorthands, Animationen

Oft ist die Kaskade korrekt – aber das Ergebnis wirkt trotzdem nicht. Typische Ursachen:

Ungültige Werte

Ungültige Deklarationen werden verworfen. Das fühlt sich an, als ob „CSS ignoriert“ – tatsächlich ist die Zeile einfach nicht anwendbar.

Shorthands überschreiben Longhands

background, font, margin usw. setzen viele Unter-Properties. Ein späteres Shorthand kann dir eine vorher gesetzte Einzel-Property überschreiben.

Custom Properties und var()

--tokens kaskadieren und vererben sich wie normale Properties. Der Wert wird aber erst beim Einsetzen mit var() aufgelöst. Fehlt der Token oder ist er nicht sinnvoll einsetzbar, kann die gesamte Deklaration ungültig werden. Darum sind Fallbacks (var(--x, fallback)) oft sinnvoll.

Computed/Used Value und Layout-Kontext

Manche Werte hängen vom Layout ab (%, auto, Grid/Flex-Verteilung). In DevTools siehst du dann manchmal einen „computed“ Wert, aber der tatsächlich verwendete („used“) entsteht erst im Layout-Schritt.

Animationen und Transitions

Während eine Animation/Transition läuft, wird der Wert „überblendet“. Wenn du einen Konflikt debuggen willst, ist es oft hilfreich, Animationen/Transitions kurz auszuschalten.

6) Web Components: Tokens, Parts, Slots – und wie @layer/@scope dazu passen

Custom Elements ohne Shadow DOM

Ohne Shadow DOM ist ein Custom Element ein normales Element. Alles aus den Abschnitten oben gilt direkt. @layer und @scope sind hier besonders nützlich, um komponentenartige Regeln sauber zu organisieren.

Mit Shadow DOM: Kapselung + kontrollierte Schnittstellen

Mit Shadow DOM gilt: Außen-CSS kommt nicht hinein. Trotzdem gibt es drei saubere Wege, von außen Einfluss zu nehmen:

  1. Tokens (Custom Properties)
    Außen setzt du Variablen am Host, innen nutzt du sie mit var(). Das ist der Standardweg für Theming.

  2. ::part()
    Die Komponente markiert interne Elemente als „Parts“. Außen kannst du diese Parts gezielt stylen, ohne die Kapselung grundsätzlich aufzuheben.

  3. Slots + ::slotted()
    Slotted Inhalte sind Light DOM und können von außen normal gestylt werden. Innen kann die Komponente begrenzt über ::slotted() Einfluss nehmen (aber nur auf direkt geslottete Elemente).

@layer/@scope im Zusammenspiel

  • Außen organisierst du Host/Part/Slot-Regeln sauber: @layer als Architektur-Priorität (z. B. components vs theme) und @scope als lokaler Zaun (z. B. eine Variante nur innerhalb eines bestimmten Bereichs).
  • Innen kann die Komponente ihre eigenen Layers/Scopes einsetzen, um interne Regeln stabil zu halten. Von außen steuerst du dann nicht über immer stärkere Selektoren, sondern über Tokens/Parts/Slots.

7) Eine kurze, verlässliche Reihenfolge fürs Debugging

Wenn du wissen willst „wo kommt dieser Wert her“, geh immer so vor – und denk daran: pro Property:

  1. Ist die Regel aktiv? (@media/@supports/@container, State)
  2. Darf sie matchen? (@scope, Shadow-Grenze)
  3. Welche Herkunft/Wichtigkeit? (!important, UA/User/Author, Inline)
  4. Welche Layer-Ordnung? (@layer)
  5. Welche Scope-Nähe? (innerer Scope gewinnt)
  6. Welche Spezifität? (inkl. :is/:where/:not/:has, Komma-Listen)
  7. Welche Reihenfolge im Code?
  8. Wenn es trotzdem nicht wirkt: ungültig? Shorthand? var()? Layout/used value? Animation/Transition?

Mit dieser Pipeline im Kopf wird CSS sehr gut erklärbar: Du kannst bei jeder Eigenschaft sagen, welche Regeln kandidieren, welche Stufe entscheidet und warum.



Als erster einen Kommentar schreiben.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert