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:
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.
Bevor eine Regel gewinnen kann, muss sie überhaupt aktiv sein und matchen.
@media, @supports, @containerViele 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.
Bei Web Components mit Shadow DOM gibt es eine zusätzliche Matching-Regel:
my-widget { … }),Das ist keine Frage von Spezifität. Die Selektoren dürfen dort schlicht nicht matchen.
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.
!importantCSS unterscheidet, aus welcher Quelle eine Regel stammt:
<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.
style="" ist eine eigene KraftInline-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.
@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:
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.
@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.
Wenn Herkunft/!important, Inline, Layer und Scope keine Entscheidung erzwingen, kommen die beiden Klassiker:
Spezifität ist ein Zählsystem. Am besten denkt man in vier Spalten:
(Inline, IDs, Klassen/Attribute/Pseudoklassen, Typen/Pseudoelemente)
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:
`,>,+,~`) zählen nicht.* zählt nicht.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.
:where, :is, :not, :hasHier 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:
:where(...) ausdrücken (bleibt schwach),Nicht jede Eigenschaft muss irgendwo explizit gesetzt sein.
color, font-*, line-height) kommen vom Eltern-Element, wenn du sie nicht setzt.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.
var(), Shorthands, AnimationenOft ist die Kaskade korrekt – aber das Ergebnis wirkt trotzdem nicht. Typische Ursachen:
Ungültige Deklarationen werden verworfen. Das fühlt sich an, als ob „CSS ignoriert“ – tatsächlich ist die Zeile einfach nicht anwendbar.
background, font, margin usw. setzen viele Unter-Properties. Ein späteres Shorthand kann dir eine vorher gesetzte Einzel-Property überschreiben.
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.
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.
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.
@layer/@scope dazu passenOhne 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 gilt: Außen-CSS kommt nicht hinein. Trotzdem gibt es drei saubere Wege, von außen Einfluss zu nehmen:
Tokens (Custom Properties)
Außen setzt du Variablen am Host, innen nutzt du sie mit var(). Das ist der Standardweg für Theming.
::part()
Die Komponente markiert interne Elemente als „Parts“. Außen kannst du diese Parts gezielt stylen, ohne die Kapselung grundsätzlich aufzuheben.
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@layer als Architektur-Priorität (z. B. components vs theme) und @scope als lokaler Zaun (z. B. eine Variante nur innerhalb eines bestimmten Bereichs).Wenn du wissen willst „wo kommt dieser Wert her“, geh immer so vor – und denk daran: pro Property:
@media/@supports/@container, State)@scope, Shadow-Grenze)!important, UA/User/Author, Inline)@layer):is/:where/:not/:has, Komma-Listen)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