Wie die Kaskade einen Gewinner auswählt: Bedeutung, Spezifität und Quellreihenfolge
Erfahren Sie, wie Browser konkurrierende CSS-Deklarationen auflösen, wie Sie die Spezifität als Vier-Teile-Vergleich lesen können und warum Hover-Regeln sowie !important Sie so oft überraschen.
Wenn mehrere CSS-Regeln auf dasselbe Element zielen und dieselbe Eigenschaft festlegen, kann der Browser sie nicht alle anwenden. Er benötigt eine deterministische Methode, um genau eine Deklaration auszuwählen – dieser Prozess erklärt eine ganze Klasse von Fehlern vom Typ „Warum wird mein Stil ignoriert?“ Am Ende dieses Leitfadens werden Sie in der Lage sein, die Spezifität eines Selektors zu berechnen, vorherzusagen, welche Deklaration gewinnt, und hartnäckige Fälle wie eine :hover-Regel, die niemals ausgelöst wird, ohne den Einsatz von !important, zu beheben.
Wo dies in der Render-Pipeline einzuordnen ist
Auf höherer Ebene wandelt der Browser Markup und Styles durch eine Reihe von Schritten in Pixel um. HTML wird in das DOM, CSS in das CSSOM解析, die beiden werden zu einem Renderbaum kombiniert, und anschließend erzeugen Layout- und Paint-Prozesse das, was Sie sehen:
HTML
↓
DOM
↓
CSS
↓
CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels
Im CSS-Verarbeitungsprozess verbergen sich drei miteinander verbundene Fragen. Erstens: Wenn mehrere Deklarationen um die gleiche Eigenschaft konkurrieren, welche gewinnt? Zweitens: Was wird tatsächlich als Wert ausgewählt, sobald ein Gewinner bestimmt ist? Drittens: Was passiert, wenn ein Element überhaupt keinen Wert für eine Eigenschaft hat? Die Antworten sind die Kaskadierung (mit der Spezifität als Kernprinzip), die Wertverarbeitung und die Vererbung. Oft werden sie als unzusammenhängende Themen behandelt, doch im Browser handelt es sich dabei um aufeinanderfolgende Schritte derselben Aufgabe. Diese Anleitung konzentriert sich auf die erste Frage. Wenn Sie mehr über den weiteren Ablauf nach der Auswertung der Styles erfahren möchten, lesen Sie wie der Browser zeichnet und wo React hineinpasst.
Regeln, Deklarationen und konkurrierende Werte
Zuerst ein wenig Wortschatz. Eine CSS-Regel besteht aus einem Selektor, gefolgt von einem Deklarationsblock:
.button {
background-color: blue;
}
In dieser Regel ist .button der Selektor, background-color: blue; eine Deklaration, background-color die Eigenschaft und blue der Wert. Der Wert genau so, wie Sie ihn geschrieben haben, wird Deklarierter Wert genannt.
Eine echte Stylesheet enthält oft mehrere Deklarationen für dieselbe Eigenschaft am selben Element. Eine Regel kann auf alle Buttons abzielen:
button {
background-color: red;
}
während andere, vielleicht in einer anderen Datei, auf Buttons im Allgemeinen sowie auf einen bestimmten Button anhand seines IDs abzielen:
button {
background-color: blue;
}
#submit {
background-color: green;
}
Falls ein einzelnes <button id="submit"> zu allen drei Kriterien passt, welche Hintergrundfarbe sollte es dann erhalten? Die Lösung dieser Frage obliegt der Kaskade. Sie löst Konflikte, indem sie die Bedeutung jeder Deklaration, die Spezifität des zugehörigen Selektors sowie die Reihenfolge der Deklarationen berücksichtigt. Nachdem die Bedeutung der Deklarationen einbezogen wurde, entscheidet in der Regel die Spezifität über das Endergebnis.
In der vollständigen Kaskadenlogik der Spezifikation werden außerdem die Herkunft einer Stylesheet-Datei (Browserstandarde, Benutzerstile, Autostile) sowie in modernem CSS die mit @layer deklarierten Kaskadenebenen berücksichtigt. Bei alltäglichen Autostylesheets ohne solche Ebenen sind Bedeutung, Spezifität und Reihenfolge der Quellen die drei Faktoren, auf die man sich stützen muss.
Was misst die Spezifität?
Spezifität ist das Maß des Browsers dafür, wie präzise ein Selektor ein Element anspricht. Nicht alle Selektoren haben denselben Gewichtungsfaktor. Vergleichen Sie einen Typ-Selektor:
p {
color: red;
}
einen Klassenselektor:
.text {
color: blue;
}
und einen ID-Selektor:
#title {
color: green;
}
Falls alle drei auf dasselbe Element zutreffen, ordnet der Browser sie nach einer festgelegten Hierarchie der Selektorarten von stärkst bis schwächst ein:
Inline styles
↓
IDs
↓
Classes / pseudo-classes / attributes
↓
Elements / pseudo-elements
Wenn die konkurrierenden Deklarationen dieselbe Bedeutung haben, gewinnt der spezifischere Selektor. Hier würde die ID-Regel gewinnen und der Text wäre grün.
Eine präzise Anmerkung zur obersten Zeile: Inline-Stile, die über das style-Attribut gesetzt werden, sind keine Selektoren, und die aktuelle Spezifikation behandelt sie als einen separaten Schritt, der jeder auf Selektoren basierenden Autorendeklaration Vorrang einräumt. Ihr Modellieren als die höchste „Spalte“ der Spezifität, wie es üblicherweise geschieht, führt in der Praxis zum gleichen Ergebnis, weshalb dieser Leitfaden dieses praktische Modell beibehält.
Spezifität als vier Spalten lesen
Der häufigste Irrtum ist, dass die Spezifität eine einzige Zahl sei, die man addieren kann. Besser ist es, sie als ein Tupel aus vier Zahlen zu verstehen, jeweils eine pro Kategorie:
Inline | IDs | Classes | Elements
Für einen gegebenen Selektor zählt man, wie viele Teile in jede Kategorie fallen. Ein einfacher Klassenselektor ist der einfachste Fall:
.button {
background: blue;
}
Er enthält keinen Inline-Stil, keine ID, nur eine Klasse und keine Elemente:
Inline styles → 0
IDs → 0
Classes → 1
Elements → 0
was man kompakt wie folgt schreiben kann:
0, 0, 1, 0
Nun nehmen wir einen komplexeren Selektor:
nav#main .button div {
background: green;
}
Er enthält eine id (#main), eine Klasse (.button) sowie zwei Typ-Selektoren (nav und div); daher beträgt seine Spezifität:
0, 1, 1, 2
Zum Vergleich zweier Selektoren liest der Browser diese Tupel von links nach rechts, beginnend mit der bedeutendsten Spalte. Die erste Spalte, in der sie unterschiedlich sind, bestimmt das Ergebnis, und die nachfolgenden Spalten spielen keine Rolle mehr. Deshalb überwiegt eine einzelne id jede Anzahl von Klassen und eine einzelne Klasse jede Anzahl von Typ-Selektoren: Es gibt keine Übertragung von Werten aus einer Spalte in die nächste, sodass zehn Klassen niemals zu einer id hinzugefügt werden können. Man muss sich keine Arithmetik merken, sondern nur die Reihenfolge des Vergleichs behalten.
Ausarbeitung eines tatsächlichen Konflikts
Betrachten Sie einen Button mit sowohl einer Klasse als auch einem ID. Beachten Sie, dass es sich um reine HTML-Markup handelt, weshalb hier class statt von JSXs className verwendet wird:
<button class="button" id="submit">
Don't Click
</button>
Nun nehmen wir an, die Stylesheet enthält eine Klassenerklärung:
.button {
background: blue;
}
zusammen mit mehreren anderen Regeln, darunter ein Typ-Selektor, ein langer Abstammungs-Selektor sowie eine ID-Plus-Klasse-Regel mit Hover-Zustand:
button {
background: purple;
}
nav#main .button div {
background: green;
}
#submit.button:hover {
background: yellow;
}
Sie alle deklarieren background, wodurch es zu Konflikten kommt. Der Browser wählt nicht einfach die letzte angegebene Regel aus; zuerst wird die Spezifität verglichen.
Ein Detail in diesem Block ist leicht zu übersehen: nav#main .button div bezieht sich tatsächlich auf ein div, das innerhalb eines Elements mit der Klasse button eingebettet ist, und nicht auf die Schaltfläche selbst. Da das Subjekt eines Selektors der rechtsstehendste Teil ist, passt diese Regel niemals auf unsere <button>, unabhängig von ihrer Spezifität. Zu prüfen, ob eine Regel überhaupt passt, ist immer der erste Schritt bei der Fehlersuche.
Bei den Regeln, die tatsächlich passen, vergleichen Sie einen Klassenselektor:
.button
mit einem reinen Typenselektor:
button
Der erste enthält eine Klasse, der zweite nur ein Element. Daher:
.button
hat Vorrang:
button
und die Schaltfläche ist blau und nicht lila, obwohl die Regel für Lila später kommt.
Fügen Sie ein id hinzu:
#submit.button
Durch das id wird dieser Selektor in eine höhere Spalte gelegt als alles, was ausschließlich aus Klassen und Typen besteht. Bei einem Vergleich von links nach rechts entscheidet die id-Spalte sofort, weshalb ein einziges id einen aus vielen Klassen bestehenden Selektor übertrifft.
Warum eine korrekte :hover-Regel nichts bewirken kann
Dieser Fall verursacht während des Debuggens viel Verwirrung. Beginnen Sie mit einer Basisregel für die Schaltfläche:
#submit.button {
background: red;
}
und einer :hover-Regel für dasselbe Element:
#submit.button:hover {
background: yellow;
}
Die :hover-Regel enthält ein id, eine Klasse und eine Pseudo-Klasse, was ihr mehr Spezifität verleiht als der Basisregel; dadurch wird die Schaltfläche wie erwartet gelb, wenn man mit der Maus darüberfährt.
Stellen Sie sich nun vor, ein anderer Teil des Codebases gestaltet dieselbe Schaltfläche mit einem viel längeren Selektor:
nav#main div#container #submit.button {
background: red;
}
während die :hover-Regel unverändert bleibt:
#submit.button:hover {
background: yellow;
}
Die :hover-Regel enthält weiterhin eine Pseudo-Klasse:
:hover
Pseudo-Klassen zählen zwar in der Klassenspalte mit, fügen aber nur einen Punkt auf Klassenebene hinzu. Der längere Selektor enthält drei IDs im Vergleich zu einem bei der hover-Regel, wodurch er bereits in der ID-Spalte gewinnt, bevor überhaupt die Klassen verglichen werden. Das Ergebnis ist eine :hover-Regel, die syntaktisch perfekt ist, zum Element passt, aber dennoch nichts auf dem Bildschirm ändert.
Die Lektion daraus ist, dass Pseudo-Klassen selten der Grund dafür sind, wenn Interaktionszustände nicht funktionieren. Das eigentliche Problem liegt meist darin, dass eine andere Deklaration spezifischer ist. Die Entwicklertools der Browser machen das sichtbar: Das Styles-Fenster listet alle passenden Regeln auf und streicht die unterlegenen Deklarationen durch, was genau zeigt, welcher Selektor den Ihren übertrifft.
Gleiche Prioritäten werden durch die Reihenfolge der Quelle entschieden
Mannchmal haben zwei Selektoren dieselbe Spezifität. Nehmen wir beispielsweise zwei Regeln mit dem gleichen Klassenselektor:
.button {
background: red;
}
.button {
background: blue;
}
Sie sind gleichermaßen spezifisch und gleichermaßen wichtig, sodass keines der ersten beiden Ebenen entscheiden kann. Der Browser greift dann auf die Quellreihenfolge zurück: Die später auftretende Deklaration gewinnt. Bei dieser Reihenfolge:
.button {
background: red;
}
.button {
background: blue;
}
die Schaltfläche wird blau.
Den gesamten Entscheidungsprozess kann man als Abfolge von Entscheidungskriterien für unentschiedene Fälle darstellen:
Importance
↓
Specificity
↓
Source Order
Jede Ebene wird nur konsultiert, wenn die vorherige keinen Gewinner ergeben konnte. Zuerst kommt die Wichtigkeit; falls diese gleich ist, entscheidet die Spezifität; wenn auch das unentschieden bleibt, gewinnt die letzte Deklaration.
Der Preis für den Einsatz von !important
Fast jeder Entwickler hat dieses Ausweichmittel mindestens einmal verwendet:
color: red !important;
Durch das Hinzufügen von !important wird die Wichtigkeit einer Deklaration erhöht. Da die Wichtigkeit vor der Spezifität geprüft wird, kann eine auf diese Weise markierte Deklaration einer mit weitaus höherer Spezifität überlegen sein. Zum Beispiel:
.button {
background: purple !important;
}
gewinnt gegenüber einer normalen Deklaration bei einem langen, mit id-Attributen gesättigten Selektor. Wenn zwei !important-Deklarationen miteinander konkurrieren, vergleicht der Browser zunächst ihre Spezifität und anschließend ihre Reihenfolge.
Genauso diese Macht macht es riskant. Ein bekanntes Debugging-Problem sieht so aus:
"My style isn't working."
↓
"Let's increase the specificity."
↓
"Still not working."
↓
"Let's add !important."
↓
"It works!"
Der Stil wird schließlich angezeigt, doch der zugrunde liegende Konflikt ist nicht beseitigt worden; er wird an die nächste Person weitergegeben, die diese Eigenschaft überschreiben muss und nun mit ihrem eigenen !important kämpfen muss. Je mehr solcher Fälle anhäufen, desto schwieriger wird es, die Stylesheet-Struktur zu verstehen. Verwenden Sie !important nur als letztes Mittel, und betrachten Sie ein plötzliches Bedürfnis danach als Zeichen dafür, dass das CSS wahrscheinlich überarbeitet werden sollte.
Bevor Sie etwas eingeben:
!important
stellen Sie eine nützlichere Frage: Warum gewinnt meine Deklaration nicht? Prüfen Sie anschließend die Ebenen in dieser Reihenfolge:
- Ist eine konkurrierende Deklaration wichtiger?
- Ist ein konkurrierender Selektor spezifischer?
- Kommt eine konkurrierende Deklaration später im Quellcode vor?
Es gibt tatsächlich legitime Verwendungsfälle, wie z. B. Utility-Klassen, die stets angewendet werden sollen, oder das Überschreiben von inline-Stilen, die durch ein von Dritten bereitgestelltes Widget eingefügt werden und nicht geändert werden können – doch solche Maßnahmen sollten bewusst erfolgen und nicht reflexartig.
Selbstverständlich funktionierende Selektoren schreiben
Hier spielt auch die Wartbarkeit eine Rolle. Wenn ein Stil nicht angewendet wird, ist es verlockend, den Selektor immer länger zu machen, bis er funktioniert:
body div section nav ul li a.button {
color: red;
}
Das funktioniert zwar, doch jedes zusätzliche Element erhöht die Hürde für zukünftige Überschreibungen, bindet den Stil an eine bestimmte DOM-Struktur und macht die Stylesheet-Datei schwerer lesbar. Anstatt zu fragen, wie man einen Selektor um jeden Preis zum Erfolg bringt, sollte man sich fragen, wie das CSS so organisiert werden kann, dass die beabsichtigte Deklaration von selbst Vorrang hat. Dabei helfen es, die meisten Selektoren auf eine einzige Klasse zu beschränken, IDs für das Styling zu vermeiden sowie spezifischere Überschreibungen in der Nähe der betroffenen Regeln zu gruppieren.
Dies ist besonders wichtig in großen Codebasen, in denen viele Personen CSS schreiben. Die Spezifität dient dazu, die Ergebnisse vorhersehbar zu machen – nicht dazu, einen Wettlauf der Selektoren auszulösen.
Nutzung der Quellreihenfolge mit Stylesheets von Drittanbietern
Die Quellreihenfolge wird zu einem praktischen Werkzeug, wenn man eigene Styles mit einem Reset-File oder einem Stylesheet von Drittanbietern kombiniert. Solche Dateien legen in der Regel Styles für gängige Elemente fest, und man möchte, dass eigene Regeln diese überschreiben. Das Erstellen der eigenen Stylesheet nach diesen Dateien macht das einfach:
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
Falls die Selektoren dieselbe Spezifität wie die der Bibliothek haben, gewinnt das später geladene File – daher führt das Einfügen von style.css nach reset.css dazu, dass die eigenen Deklarationen wirken, ohne dass die Selektoren übermäßig komplex werden.
Auf die Reihenfolge zu vertrauen hat auch seinen Preis. Wenn jemand später die <link>-Tags oder die Imports in einem Entry Point eines Bundlers erneut anordnet, können sich die Styles stillschweigend ändern. Wo immer möglich sollten Sie Regeln bevorzugen, deren Spezifität klar zeigt, welcher Wert gewinnen soll, und die Reihenfolge der Quellen als letzten Entscheidungsfaktor nutzen, wie es ursprünglich vorgesehen war. Kaskadierte Ebenen, sofern Ihre Zielbrowser sie unterstützen, sind eine deutlichere Art, auszudrücken „die Bibliothek kommt zuerst, unsere Styles danach“.
Nach dem Gewinner: der kaskadierte Wert
Zu diesem Zeitpunkt ist die erste Frage beantwortet. Der Browser hat jede Deklaration für die Eigenschaft gesammelt, zunächst die Wichtigkeit, dann die Spezifität und schließlich die Reihenfolge der Quellen berücksichtigt und sich für einen Wert entschieden. Dieser gewinnende Wert wird Kaskadierter Wert genannt.
Doch die Arbeit ist noch nicht ganz erledigt. Angenommen, die gewinnende Deklaration lautet:
width: 66%;
Eine solche Prozentsangabe kann nicht direkt dargestellt werden; der Browser muss sie weiterverarbeiten, indem er sie im Zusammenhang mit dem enthaltenden Block auswertet, um eine tatsächliche Länge zu ermitteln. Diese Werteverarbeitung zusammen mit der Vererbung von Eigenschaften, für die es überhaupt keine Deklaration gibt, stellt die nächste Stufe bei der Umwandlung von CSS in Pixel dar.
Wichtige Erkenntnisse
- Die Kaskade löst Konflikte in einer festgelegten Reihenfolge auf: Wichtigkeit, anschließend Spezifität und dann Quellreihenfolge.
- Spezifität wird durch einen Vier-Spalten-Vergleich (inline, IDs, Klassen/Pseudo-Klassen/Attribute, Elemente/Pseudo-Elemente) von links nach rechts durchgeführt; Werte aus niedrigeren Spalten fließen niemals in höhere Spalten über.
- Stellen Sie sicher, dass eine Regel tatsächlich mit dem Element übereinstimmt, bevor Sie ihre Spezifität vergleichen; der rechtsstehende Teil eines Selektors gibt an, auf welches Element sich die Regel bezieht.
:hover- oder andere Pseudo-Klasse-Regel fügt lediglich ein Klassen-Ebene-Gewicht hinzu und kann von einer spezifischeren Basierungsregel übertroffen werden.!important gewinnt durch eine Änderung der Wichtigkeit, nicht indem es den Konflikt löst; verwenden Sie es bewusst und sparsam.