CSS-Selektoren von CSS2 bis Level 4: Überblick mit Beispielen

CSS-Selektoren von CSS2 bis Level 4: Überblick mit Beispielen

Selektoren entscheiden, welche Elemente eine CSS-Regel überhaupt trifft. Wer sie gut beherrscht, braucht weniger Klassen im HTML, weniger JavaScript für Zustände wie ein fehlerhaft ausgefülltes Formular und hat seltener Ärger mit Regeln, die sich gegenseitig überschreiben. Seit 2023 funktionieren auch die mächtigsten Neuerungen aus Selectors Level 4 in allen großen Browsern, allen voran :has().

Von CSS2 zu Selectors Level 4

Die CSS2-Empfehlung des W3C vom 12. Mai 1998 widmete den Selektoren ein eigenes Kapitel. Es enthielt bereits Typ-, Klassen- und ID-Selektoren, den Universalselektor *, Attributselektoren in vier Formen ([att], [att=wert], [att~=wert], [att|=wert]), die Kombinatoren für Nachfahren (Leerzeichen), Kinder (>) und direkte Geschwister (+), die Pseudoklassen :first-child, :link, :visited, :hover, :active, :focus und :lang() sowie die Pseudoelemente :first-line, :first-letter, :before und :after.

Selectors Level 3, seit dem 6. November 2018 W3C-Empfehlung, ergänzte unter anderem die Attributvergleiche ^=, $= und *=, den allgemeinen Geschwisterkombinator ~, die Strukturpseudoklassen wie :nth-child(), :last-child und :empty, dazu :not(), :target, :checked und die Doppelpunkt-Schreibweise :: für Pseudoelemente. Selectors Level 4 ist formal weiterhin ein Arbeitsentwurf (zuletzt vom 22. Januar 2026), große Teile davon sind aber längst in allen Browsern umgesetzt: :is(), :where(), :has(), Selektorlisten in :not() und :nth-child(… of S).

Selektoren und ihre Treffer im Überblick

Die Tabelle bezieht sich auf ein gedachtes Dokument mit Navigation, Artikel und Formular. Die Spalte Spezifität folgt der Schreibweise ID-Klasse-Typ; wie sich daraus die Rangfolge ergibt, erklärt der Beitrag zu Kaskade, Spezifität und Vererbung.

SelektorTrifftSpezifität
palle Absätze0-0-1
.hinweisalle Elemente mit der Klasse hinweis0-1-0
#suchedas eine Element mit der ID suche1-0-0
a[href^='https://']Links, deren Ziel mit https:// beginnt0-1-1
a[href$='.pdf']Links auf PDF-Dateien0-1-1
nav aLinks irgendwo innerhalb von nav0-0-2
ul > linur direkte Listenpunkte, keine verschachtelten0-0-2
h2 + pder erste Absatz direkt nach einer h20-0-2
h2 ~ palle späteren Geschwister-Absätze einer h20-0-2
li:nth-child(odd)1., 3., 5. Listenpunkt0-1-1
li:nth-child(-n+3)die ersten drei Listenpunkte0-1-1
input:not([type='checkbox'], [type='radio'])alle Eingabefelder außer Checkboxen und Radiobuttons0-1-1
:is(h2, h3) aLinks in Überschriften zweiter oder dritter Ebene0-0-2
:where(.karte) aLinks in Karten, ohne Spezifität der Klasse0-0-1
form:has(:user-invalid)Formulare mit einem vom Nutzer falsch ausgefüllten Feld0-1-1
li::markerdas Aufzählungszeichen jedes Listenpunkts0-0-2

Attributselektoren und Kombinatoren richtig einsetzen

Attributselektoren sind ideal für Inhalte, die man nicht mit Klassen versehen kann, etwa Links aus einem Redaktionssystem. Werte wie href vergleicht der Browser mit Groß- und Kleinschreibung; ein angehängtes i vor der schließenden Klammer schaltet das ab: a[href$='.pdf' i] trifft auch .PDF.

Der häufigste Fehler bei Kombinatoren ist ein zu breiter Nachfahrenselektor. .menu li trifft auch die Punkte eines Untermenüs, .menu > li nur die oberste Ebene. Wer Navigationen baut, erspart sich mit dem Kindkombinator viele Überschreibungsregeln; Hinweise zum Aufbau stehen im Artikel Website-Navigation gestalten.

Pseudoklassen: Zustand, Struktur, Logik

:hover, :focus-visible und Formularzustände

:hover reagiert auf den Mauszeiger, nützt auf Touchgeräten aber wenig. Für sichtbare Tastaturfokusringe ist :focus-visible besser als :focus, weil der Browser selbst entscheidet, wann ein Fokusrahmen sinnvoll ist, und Mausklicks auf Buttons keinen Rahmen erzeugen. Bei Formularen meldet :invalid Fehler sofort beim Laden, also bevor jemand etwas eingetippt hat; :user-invalid greift erst, wenn jemand mit dem Feld interagiert oder das Formular abzuschicken versucht hat, und ist seit November 2023 Baseline. Für Fehlermarkierungen ist es fast immer die bessere Wahl.

:nth-child() und die Formel An+B

Die Zählung beginnt bei 1. 2n+1 (oder odd) trifft 1, 3, 5, n+7 alle ab dem siebten, -n+3 die ersten drei. Neu in Level 4 ist der Zusatz of S: li:nth-child(odd of .sichtbar) zählt nur Elemente mit der Klasse sichtbar. Damit bleiben Zebrastreifen in gefilterten Tabellen korrekt, was mit :nth-child(odd) allein nicht klappt, weil versteckte Zeilen mitgezählt werden.

:not(), :is() und :where()

Alle drei nehmen Selektorlisten. Der Unterschied liegt in der Spezifität: :is() und :not() übernehmen die des spezifischsten Arguments, :where() zählt immer null. Das macht :where() zum Werkzeug für Basis-Stile in Bibliotheken und Resets, die sich später mit einem einfachen Selektor überschreiben lassen sollen. Ein zweiter Vorteil: :is() und :where() verwenden eine fehlertolerante Liste. Ist ein Selektor darin unbekannt, fällt nur er weg, nicht die ganze Regel. Bei einer normalen Liste mit Komma oder bei :not() verwirft der Browser dagegen die komplette Regel.

Eine Falle bei :not(): body :not(table) a trifft auch Links in Tabellen, weil td und tr keine Tabellen sind. Richtig ist body a:not(table a).

:has(), der lange vermisste Elternselektor

:has() wählt ein Element danach aus, was in ihm steckt oder ihm folgt. Damit lassen sich Zustände ohne JavaScript abbilden:

/* Karte mit Bild bekommt ein zweispaltiges Layout */
.karte:has(> img) {
  display: grid;
  grid-template-columns: 8rem 1fr;
}



/* Feldgruppe rot umranden, wenn ein Feld falsch ausgefüllt ist */
fieldset:has(:user-invalid) {
  border-color: #b00020;
}



/* Überschrift, auf die direkt ein Untertitel folgt */
h2:has(+ .untertitel) {
  margin-bottom: 0.25rem;
}

Einschränkungen: :has() lässt sich nicht in sich selbst verschachteln, und Pseudoelemente sind als Argument nicht erlaubt.

Pseudoelemente: ::before, ::after und ::marker

::before und ::after erzeugen zusätzliche Boxen am Anfang und Ende eines Elements, sichtbar nur mit einer content-Angabe. Für Schmuckelemente und Icons sind sie ideal, für echte Inhalte nicht: Text aus content lässt sich nicht markieren und gehört nicht zum HTML-Dokument. Die alte Schreibweise mit einem Doppelpunkt funktioniert für diese vier CSS2-Pseudoelemente weiterhin, neue wie ::marker gibt es nur mit zwei.

::marker gestaltet Aufzählungszeichen und Nummern, erlaubt aber nur wenige Eigenschaften, darunter Farbe, Schriftangaben und content. Genau bei content hakt es: Safari unterstützt ::marker nur teilweise, eigene Marker-Inhalte sind laut WebKit-Bugtracker erst in der Safari Technology Preview behoben. Farbe und Schriftgröße funktionieren überall; für eigene Zeichen bleibt li::before mit list-style: none vorerst die robustere Lösung.

Browserunterstützung im Oktober 2026

FunktionChrome/EdgeFirefoxSafariStatus
:is(), :where()jajajaBaseline seit Januar 2021
Liste in :not()ab 88ab 84ab 9überall verfügbar
:nth-child(… of S)ab 111ab 113ab 9überall verfügbar seit 2023
:has()ab 105ab 121ab 15.4Baseline seit Dezember 2023, inzwischen breit verfügbar
::markerab 86ab 68teilweise (ab 11.1)laut MDN eingeschränkt verfügbar

Praktisch heißt das: :is(), :where() und :has() können in neuen Projekten ohne Fallback verwendet werden. Wer noch sehr alte Firefox- oder Safari-Versionen unterstützen muss, setzt :has() nur für Verbesserungen ein, ohne die das Layout trotzdem funktioniert.

Performance: welche Mythen nicht mehr stimmen

Alte Ratgeber warnen vor dem Universalselektor, vor Attributselektoren und vor langen Selektorketten. Für typische Websites ist das kaum messbar. Der Leitfaden auf web.dev zu Stilberechnungen nennt einen wichtigeren Faktor: die Zahl der Elemente, deren Stil bei einer Änderung neu berechnet werden muss. Wer bei jedem Klick eine Klasse am body umschaltet, verursacht mehr Arbeit als ein komplizierter Selektor, der nur in einem kleinen Bereich greift.

Echte Vorsicht verlangt :has() mit breitem Anker. MDN rät von Mustern wie body:has(.sidebar) ab, weil dann jede DOM-Änderung im ganzen Dokument eine Neuprüfung auslösen kann. Besser ist ein konkreter Container und ein eingegrenztes Argument, also .layout:has(> .sidebar). Bevor man Selektoren auf Verdacht umschreibt, lohnt eine Messung: Chrome DevTools zeigen im Performance-Panel nach Aktivierung von „Enable CSS selector stats“ für jeden Selektor die Zeit, Zahl der Prüfversuche und Treffer. Erst wenn dort ein Selektor auffällt, ist Optimieren sinnvoll.

Quellen