Über diese Website

Dieser Kasten steht auf jeder Seite in der linken Spalte. Was darin steht, änderst du in der Konfiguration unter Plugins — oder du schaltest ihn dort ab.

Über dieses Handbuch

Dieses Handbuch gehört zur Vorlage Example für QuartzControl. Es erklärt die Vorlage und führt zugleich alles vor, was sie kann.

Der Text hier kommt aus einer Markdown-Datei, nicht aus der Konfiguration.

Elemente anhand ihres Inhalts auswählen

Elemente anhand ihres Inhalts auswählen

Ziel

Ein Element soll anders aussehen, je nachdem was in ihm steckt: ob es ein Bild enthält, leer ist, mehr als fünf Einträge hat, auf eine PDF-Datei verlinkt oder vor einem bestimmten Element steht. Früher hat man dafür per JavaScript Klassen wie has-image oder is-empty gesetzt. Heute geht das meiste direkt in CSS.

Voraussetzungen

  • Grundkenntnisse zu :has() und :not().
  • Alle Rezepte hier sind Baseline widely available (Details unten).

Getestet mit / Stand

Stand 2026-10-18. Jedes Rezept habe ich mit mindestens einem Beispiel in Chrome 155 nachgestellt, Firefox und Safari nicht. Die Rezepte stammen aus den unten genannten Quellen oder sind daraus abgeleitet. Eigene Ergänzungen sind als solche gekennzeichnet.

Vorgehen

Vorab eine Grenze, damit man nicht lange sucht: CSS kann keinen Text vergleichen. Einen Selektor wie „Absatz, der das Wort Achtung enthält” gibt es nicht. CSS sieht nur die Struktur (welche Elemente wo stehen), Attribute und Zustände. Wer nach Text unterscheiden will, muss das im HTML sichtbar machen, etwa mit einer Klasse oder einem data-Attribut. (Eigene Einordnung; in keiner der Quellen ausdrücklich so formuliert.)

1. Auf ein enthaltenes Element reagieren

Der Grundfall für :has(): Der Container wird gestylt, wenn ein bestimmtes Kind darin steckt.

.karte:has(img)        { grid-template-rows: 12rem auto; }  /* mit Bild */
.karte:not(:has(img))  { padding-block-start: 2rem; }       /* ohne Bild */

↗ Beispiel in neuem Tab öffnen

Wichtig ist die Reihenfolge von :not() und :has(). Die beiden Varianten klingen ähnlich, bedeuten aber etwas völlig anderes:1

SelektorBedeutungTrifft auf
.karte:not(:has(img))Karte, die kein Bild enthältnur Karten ohne Bild
.karte:has(:not(img))Karte, die irgendein Element enthält, das kein Bild istpraktisch jede Karte mit Text

Die zweite Variante ist fast immer ein Versehen. Am besten liest man solche Selektoren von innen nach außen: :not(img) heißt „irgendein Element außer img“, und so eins hat fast jede Karte.

flowchart TD
  A["Soll die Karte gestylt werden?"] --> B{"Welcher Selektor?"}
  B -->|".karte:not(:has(img))"| C{"Enthält die Karte ein img?"}
  C -->|ja| D["nein, nicht gestylt"]
  C -->|nein| E["ja, gestylt"]
  B -->|".karte:has(:not(img))"| F{"Enthält sie irgendein Element, das kein img ist?"}
  F -->|"ja (fast immer)"| G["ja, gestylt"]
  F -->|nein| H["nein, nicht gestylt"]

2. UND und ODER verknüpfen

Mehrere :has() hintereinander bedeuten „und”, eine Liste innerhalb eines :has() bedeutet „oder”:2

.regal:has(.buch-lila):has(.buch-gelb) { … }  /* lila UND gelb */
.regal:has(.buch-lila, .buch-gelb)     { … }  /* lila ODER gelb */

↗ Beispiel in neuem Tab öffnen

3. Nach Attributen unterscheiden

Attributselektoren gibt es schon lange, richtig nützlich werden sie aber erst zusammen mit :has(). Die Varianten:3

Selektortrifft, wenn der Attributwert …
[attr]vorhanden ist (egal welcher Wert)
[attr="x"]genau x ist
[attr^="x"]mit x beginnt
[attr$="x"]mit x endet
[attr*="x"]x irgendwo enthält
[attr~="x"]in einer Leerzeichen-getrennten Liste das Wort x enthält
[attr|="x"]genau x ist oder mit x- beginnt (z. B. Sprachcodes)
[attr$="x" i]… und dabei Groß-/Kleinschreibung ignoriert

Praktische Beispiele:

a[href$=".pdf" i]::after   { content: " (PDF)"; }
a[href^="mailto:"]         { … }
a[download]                { … }  /* Download-Link */
 
/* Container reagiert auf den Link darin */
.datei:has(a[href$=".pdf" i]) { --icon: url(pdf.svg); }

↗ Beispiel in neuem Tab öffnen

Das letzte Muster stammt aus Ahmad Shadeeds Guide: Man erkennt den Dateityp am Link und leitet daraus das Icon des Containers ab.2

4. Leere Elemente erkennen

:empty trifft Elemente ohne jedes Kind. Aber Vorsicht: Auch ein Leerzeichen oder ein Zeilenumbruch zählt als Inhalt. Nur Kommentare zählen nicht.4

<div class="ergebnis"></div>               <!-- :empty trifft -->
<div class="ergebnis"><!-- leer --></div>  <!-- :empty trifft -->
<div class="ergebnis"> </div>              <!-- :empty trifft NICHT -->

↗ Beispiel in neuem Tab öffnen

Selectors Level 4 sollte das ändern, sodass reine Leerzeichen nicht mehr zählen. Laut MDN setzt das aber noch kein Browser um,4 und in Chrome 155 zählt ein Leerzeichen tatsächlich noch als Inhalt. In der Praxis heißt das: Template-Engines so einstellen, dass leere Container wirklich leer ausgegeben werden.

Zusammen mit :has() lässt sich ein ganzer Abschnitt in einen „Noch nichts da”-Zustand schalten:2

.abschnitt:has(.abschnitt-inhalt:empty) {
  display: grid;
  place-content: center;
  min-block-size: 10rem;
}

5. Nach Anzahl der Kinder unterscheiden (Quantity Queries)

Die Technik ist älter als :has(). Shadeed verweist dafür auf einen Artikel von Heydon Pickering bei A List Apart. Sie funktioniert über :nth-last-child(): li:nth-last-child(n + 5) trifft ein li, nach dem noch mindestens vier weitere kommen. So eins gibt es nur, wenn die Liste mindestens fünf Einträge hat. Früher ließen sich damit nur die Einträge selbst stylen. Mit :has() reagiert der Container:2

.galerie {
  --spalte: 300px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--spalte), 1fr));
}
.galerie:has(> :nth-last-child(n + 5)) { --spalte: 200px; }  /* ab 5 Einträgen */
.galerie:has(> :nth-last-child(n + 9)) { --spalte: 130px; }  /* ab 9 Einträgen */

↗ Beispiel in neuem Tab öffnen

Weitere Varianten (eigene Ableitungen nach demselben Prinzip):

.liste:has(> :only-child)                       { … }  /* genau ein Eintrag */
.liste:has(> :nth-child(3):last-child)          { … }  /* genau drei */
.liste:not(:has(> :nth-child(4)))               { … }  /* höchstens drei */

6. Nur bestimmte Kinder mitzählen

:nth-child() zählt normalerweise alle Geschwister. Mit of <selector> zählt es nur die, die zum Selektor passen.5 Der Klassiker: Zebrastreifen in Tabellen, bei denen ausgeblendete Zeilen das Muster nicht durcheinanderbringen.

tbody tr:nth-child(even of :not([hidden])) { background: whitesmoke; }

↗ Beispiel in neuem Tab öffnen

Ohne of würde eine versteckte Zeile mitgezählt, und zwei sichtbare Zeilen gleicher Farbe stünden direkt untereinander.

Der Unterschied zu einem vorangestellten Selektor ist klein, aber wichtig: li:nth-child(-n + 3 of .wichtig) trifft die ersten drei .wichtig-Einträge. li.wichtig:nth-child(-n + 3) trifft dagegen nur .wichtig-Einträge, die zufällig auf Position 1 bis 3 stehen.5

7. Vorherige Geschwister auswählen

CSS konnte lange nur „nach vorn” schauen: + und ~ treffen Elemente, die nach einem anderen kommen. Mit :has() geht es auch rückwärts:6

.box:has(+ .kreis)         { … }  /* direkt vor dem Kreis */
.box:has(+ * + .kreis)     { … }  /* zwei Positionen davor */
.box:has(~ .kreis)         { … }  /* alle Boxen irgendwo vor dem Kreis */
.box:has(~ * + .kreis)     { … }  /* alle davor außer der direkt davor */

Ein Alltagsbeispiel: Eine Überschrift bekommt weniger Abstand nach unten, wenn direkt ein Untertitel folgt.

h2:has(+ .untertitel) { margin-block-end: 0.25rem; }

↗ Beispiel in neuem Tab öffnen

8. Auf Zustände im Inneren reagieren

Nicht nur Elemente, auch deren Zustand lässt sich abfragen:2

.feld:has(input:user-invalid)  { border-color: crimson; }
.karte:has(button:disabled)    { opacity: 0.6; }
.karte:has(a:focus-visible)    { outline: 2px solid; }

:user-invalid meldet einen Fehler erst, nachdem jemand mit dem Feld etwas gemacht hat. :invalid dagegen schlägt schon beim Laden der leeren Seite an. Mehr dazu in Formularfehler erst nach der Eingabe anzeigen.

Warum funktioniert das?

:has() prüft die Bedingung vom Element aus, an dem es hängt. Ohne Kombinator sucht es unter den Nachfahren, mit > nur unter den direkten Kindern, mit + und ~ unter den nachfolgenden Geschwistern. Gestylt wird aber das äußere Element. Deshalb wirkt es mal wie ein „Parent Selector“, mal wie ein „Previous Sibling Selector“. Technisch ist es beide Male dieselbe Pseudoklasse.7

Fallstricke

  • :has(:not(…)) statt :not(:has(…)): der häufigste Fehler, siehe Schritt 1.
  • :empty und Leerzeichen: siehe Schritt 4.
  • Kein Text-Matching: siehe oben. Das gilt auch für [attr*="…"], das nur Attributwerte vergleicht, nicht den sichtbaren Text.
  • Performance: MDN rät, :has() nicht an sehr breite Elemente wie body, :root oder * zu hängen und die Bedingung innen eng zu fassen (> oder + statt bloßer Nachfahren). Sonst muss der Browser bei jeder DOM-Änderung große Teilbäume neu prüfen.7
  • Keine Schachtelung: :has() darf nicht in :has() stehen, und Pseudoelemente sind darin nicht erlaubt.7 Chrome 155 verwirft solche Regeln komplett.
  • Lesbarkeit: Eine Regel, die weit oben ansetzt und auf etwas tief Verschachteltes reagiert, findet man beim Debuggen schwer. Lieber das nächstliegende sinnvolle Element als Anker nehmen. (Eigene Einordnung.)

Browsersupport

Alle Bausteine sind laut web-features 3.43.0 (abgefragt 2026-10-18) Baseline widely available:

Featurewidely available seit
:has()2026-06-19
:nth-child(… of S)2025-11-09
:user-valid / :user-invalid2026-05-02
:focus-visible2024-09-14
:empty2018-01-29

Attributselektoren sind seit CSS 2 Standard; für sie nennt web-features kein eigenes Feature, darum steht hier kein Datum.

Siehe auch

Quellen

Eigene Ergänzungen: die Grenze „kein Text-Matching“, die Varianten „genau ein/drei, höchstens drei“ in Schritt 5, das Überschriften-Beispiel in Schritt 7 und der Satz zu :user-invalid und :invalid. Das Entscheidungsdiagramm in Schritt 1 fasst die Polypane-Erklärung zusammen.

Footnotes

  1. Decoding CSS Selectors – has(not) vs not(has) (Polypane), Abschnitt „Working from the inside out”, 2025-02-17. ↩

  2. CSS has() Interactive Guide (Ahmad Shadeed), Abschnitte „Logical operators”, „File download”, „Section empty state”, „Quantity queries”, „Input states”, 2024-02-23. ↩ ↩2 ↩3 ↩4 ↩5

  3. Attributselektoren (MDN), Abschnitt „Syntax”, Stand 2026-09-04. ↩

  4. empty Pseudoklasse (MDN), Einleitung und Hinweis zu Selectors Level 4, Stand 2026-07-08. ↩ ↩2

  5. nth-child() Pseudoklasse (MDN), Abschnitte „The of syntax” und „Using of selector to fix striped tables”, Stand 2026-07-17. ↩ ↩2

  6. Selecting previous siblings with CSS has() (Tobias Ahlin), datiert „09/03/2023“ (Tag/Monat-Reihenfolge unklar). ↩

  7. has() Pseudoklasse (MDN), Abschnitte „Description” und „Performance considerations”, Stand 2026-08-11. ↩ ↩2 ↩3

Weiterlesen

Diese Seite gehört zu gui-test — der Beispielvorlage für QuartzControl. Ein Überblick über alle Bereiche steht auf der Startseite.

Auf einem schmalen Bildschirm ist die Navigation oben eingeklappt.