Sie schreiben .sidebar .card { color: blue; }, laden die Seite, und die Card ist immer noch rot — wegen einer Regel, die Sie eine Stunde vorher hinzugefügt haben: .card { color: red; }. Ihre neue Regel steht später in der Datei. Sie verliert trotzdem. Der Browser vergleicht die beiden Selektoren mit einem festen Punktesystem, und .sidebar .card erhält eine höhere Punktzahl als .card, egal welcher zuletzt geschrieben wurde.
Dieses Punktesystem ist die Spezifität. Sobald Sie sie auf den ersten Blick berechnen können, wird “warum wirkt mein CSS nicht” zu Arithmetik. Mit dem Layout hat das nichts zu tun: Grid oder Flexbox entscheidet, wo ein Element sitzt; die Spezifität entscheidet, welche Regel Farbe oder Größe setzt, wenn zwei Regeln sich widersprechen.
Wie der Browser einen Selektor bewertet
Jeder Selektor hat ein Gewicht aus drei Zahlen, meist als durch Bindestriche getrennte Dreiergruppe geschrieben: ID-Klasse-Typ. Zählen Sie zusammen, was der Selektor enthält, Spalte für Spalte:
| Spalte | Was zählt | Gewicht pro Treffer |
|---|---|---|
| ID | #header | 1-0-0 |
| Klasse / Attribut / Pseudo-Klasse | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Typ / Pseudo-Element | div, h1, ::before | 0-0-1 |
Der Universalselektor * und die Kombinatoren (das Leerzeichen, >, ~, +) fügen 0-0-0 hinzu. Sie entscheiden, welche Elemente passen, nicht wie stark die Regel gewinnt.
Ein paar echte Selektoren:
| |
Um zwei Werte zu vergleichen, liest man sie von links nach rechts und stoppt bei der ersten Spalte, die sich unterscheidet. 1-1-0 schlägt 0-2-0, weil die ID-Spalte (1 gegen 0) entscheidet, bevor überhaupt jemand auf die Klassen schaut. Deshalb schlägt #main .card auch .sidebar .card .title .text, egal wie lang der zweite Selektor wird. Eine ID überragt jede Anzahl von Klassen.
Warum eine spätere CSS-Regel trotzdem verlieren kann
Zurück zum Eingangsbeispiel, mit den berechneten Zahlen:
| |
0-2-0 schlägt 0-1-0 in der Klassen-Spalte, also ist die Card blau, unabhängig von der Reihenfolge im Quelltext. Die Quelltextreihenfolge ist das Kriterium, das die meisten als Erstes heranziehen, aber es gilt nur, wenn die Dreiergruppen identisch sind:
| |
Beide Regeln erreichen 0-1-0, also gewinnt die, die im Stylesheet weiter unten steht (oder im späteren <style>-Block, oder im Stylesheet, das nach dem anderen eingebunden wurde). Das ist der einzige Fall, in dem “die letzte gewinnt” das richtige Modell ist. In kleinen Projekten funktioniert das, deshalb lernt man es als Erstes, und es hört auf zu funktionieren, sobald ein Projekt mehr als eine Handvoll Selektoren hat.
Inline-Styles stehen über allem. Ein style="color: green"-Attribut am Element schlägt jede Regel in jedem Stylesheet, wie viele IDs sie auch verkettet. Nur !important-Deklarationen können es übertrumpfen.
| |
| |
!important ist nicht Teil der Spezifitäts-Dreiergruppe. Es ist eine eigene, höher priorisierte Stufe: Jede !important-Deklaration schlägt jede normale Deklaration, Inline-Styles eingeschlossen, unabhängig vom Gewicht des Selektors. Treffen zwei !important-Deklarationen aufeinander, entscheidet die Spezifität (dann die Quelltextreihenfolge), genau wie ohne das Flag. So wird aus einem einzigen !important ein Wettrüsten. Der nächste Entwickler, der es überschreiben muss, hat keinen anderen Zug mehr als ein weiteres !important, und bald weiß niemand mehr, welche Regel eigentlich gewinnen soll.
Welche Selektoren null Spezifität hinzufügen: :where() gegen :is()
Ein paar Selektoren erlauben präzises Targeting, ohne den normalen Preis zu zahlen.
:where() trägt immer 0-0-0 bei, egal was Sie hineinschreiben:
| |
Das macht :where() zur richtigen Hülle für Standardwerte und Theme-Styles, die leicht überschreibbar bleiben sollen. Eine Komponentenbibliothek kann ihre Standardwerte innerhalb von :where() liefern, und eine einfache .card { }-Regel des Nutzers schlägt sie ohne !important.
:is(), :not() und :has() funktionieren anders. Sie fügen kein eigenes Gewicht hinzu, übernehmen aber das Gewicht ihres spezifischsten Arguments:
| |
:where() ist also kostenlos, und :is() kostet so viel wie das Schwerste, was Sie hineinschreiben, selbst wenn dieses Argument nie auf das Element zutrifft.
Wie Cascade Layers die Spezifität aushebeln
Wenn Sie Klassen und IDs stapeln, um Kämpfe zu gewinnen, die nichts damit zu tun haben, wie spezifisch ein Selektor sein sollte, braucht das Projekt @layer, nicht mehr Verschachtelung.
| |
Die Layer-Reihenfolge ergibt sich daraus, wo Sie die Namen deklarieren, nicht aus dem Gewicht der Selektoren. utilities wurde zuletzt deklariert, also gewinnt eine Regel darin gegen eine Regel in components, obwohl .text-blue dasselbe Gewicht 0-1-0 hat wie .card. Die Spezifität entscheidet weiterhin Konflikte innerhalb einer Layer. Zwischen Layers gewinnt die Layer-Reihenfolge ohne Widerspruch, und die Spezifität wird nie befragt.
Eine Regel überrascht hier fast jeden beim ersten Mal: Bei normalen (nicht !important) Deklarationen schlagen Styles außerhalb jeder Layer Styles innerhalb einer Layer. Verschieben Sie Ihr Komponenten-CSS in @layer components, lassen aber eine einzelne Überschreibung ohne Layer übrig, gewinnt diese nicht-layerisierte Regel, egal in welcher Reihenfolge Sie die Layers deklariert haben. Bei !important-Deklarationen kehrt sich die Priorität um: Die Important-Regeln der zuerst deklarierten Layer schlagen die späteren, und nicht-layerisierte Important-Styles rangieren am niedrigsten von allen. Die Umkehrung ist Absicht, und das ist der Teil der Spezifikation, den man nachlesen sollte, bevor man sich darauf verlässt.
Browser-Unterstützung ist seit einiger Zeit kein Argument mehr gegen Layers: @layer läuft seit 2022 in jeder wichtigen Engine. Die eigentlichen Kosten sind organisatorisch. Das Team muss sich vorab auf eine Layer-Reihenfolge einigen (reset, base, components, utilities ist ein gängiges Schema) und neues CSS in die richtige Layer einordnen. Das ist eher ein Prozessproblem als ein Syntaxproblem.
Häufige Fehler mit CSS-Spezifität
- Spezifität mit mehr Spezifität bekämpfen. Jede Überschreibung fügt eine Klasse oder ID mehr hinzu als die vorherige, bis eine Farbänderung
#app .page .section .card.is-active.is-selectedbraucht. Das ist dasselbe Fehlermuster wie Spaghetti-Code: Jede Korrektur fügt Gewirr hinzu, statt es zu entfernen. Die Lösung ist fast nie ein schwererer Selektor. Es ist ein flacherer, oder eine Layer. - Beim ersten Konflikt zu
!importantgreifen. Das gewinnt den unmittelbaren Kampf und bereitet den nächsten vor, denn die nächste Überschreibung hat dann keinen normalen Spezifitäts-Zug mehr übrig. - Vergessen, dass Pseudo-Klassen zählen.
:hover,:focus,:nth-child()und das Argument von:not()fügen alle0-1-0hinzu. Ein einfachesa { color: blue; }weiter unten in der Datei überschreibta:not(.disabled) { color: red; }darüber nicht, weil die:not()-Version schwerer ist. - Annehmen, der vererbte Wert gewinne, weil sich die Regel “stärker” anfühlt. Eine Regel, die ein Element direkt anspricht, schlägt immer einen vererbten Wert, unabhängig von der Spezifität der Regel, aus der die Vererbung stammt. Zeigt ein Kind-Element die falsche Farbe, suchen Sie zuerst eine Regel, die dieses Kind direkt anspricht, bevor Sie die Vererbung verdächtigen.
:is()in einen Selektor mischen und erwarten, dass er billig bleibt.:is()übernimmt das Gewicht seines schwersten Arguments, also ist:is(.card, #sidebar)in Wirklichkeit ein getarnter1-0-0-Selektor.
Spezifität in den DevTools prüfen
Sie müssen Spalten nicht von Hand zählen, wenn ein Bug direkt vor Ihnen liegt. Inspizieren Sie das Element in den Chrome- oder Firefox-DevTools und öffnen Sie das Styles-Panel (Rules in Firefox). Jede passende Regel ist in Kaskaden-Reihenfolge aufgelistet, überschriebene Deklarationen sind durchgestrichen. Fahren Sie mit der Maus über einen Selektor, und beide Browser zeigen seine Spezifität an. Von dort aus sehen Sie in Sekunden, ob es sich um eine Spezifitätslücke, ein Layer-Reihenfolge-Problem oder einfache Quelltextreihenfolge zwischen zwei gleich gewichteten Regeln handelt.
Wenn das nächste Mal eine Regel ohne offensichtlichen Grund verliert, berechnen Sie beide Dreiergruppen, bevor Sie eine Klasse oder ein !important hinzufügen, um sie zu erzwingen. Meist ist die richtige Korrektur, dem gewinnenden Selektor Gewicht zu nehmen, statt dem, der eigentlich gewinnen sollte, noch mehr aufzuladen.