:has(): die Familienauswahl

Seit Anbeginn der Zeit (in CSS-Begriffen) haben wir mit einer Kaskade in verschiedenen Bedeutungen gearbeitet. Unsere Stile bilden ein „Cascading Style Sheet“ (CSS). Und unsere Auswahlmöglichkeiten sind ebenfalls kaskadierend. Sie können auch seitlich ausgerichtet sein. In den meisten Fällen gehen sie nach unten. Aber niemals nach oben. Seit Jahren haben wir uns eine „Elternauswahl“ gewünscht. Und jetzt ist es endlich soweit! In Form eines :has()-Pseudo-Selektors.

Die CSS-Pseudoklasse :has() steht für ein Element, wenn einer der als Parameter übergebenen Selektoren mit mindestens einem Element übereinstimmt.

Es ist aber mehr als nur eine Auswahl für „übergeordnete Elemente“. Das ist eine gute Art, das zu vermarkten. Die weniger ansprechende Möglichkeit ist die Auswahl „Bedingte Umgebung“. Aber das klingt nicht ganz so gut. Was ist mit der Auswahl „Familie“?

Unterstützte Browser

Bevor wir fortfahren, sollten wir noch kurz auf den Browsersupport eingehen. Es ist noch nicht ganz so weit. Aber es geht voran. Firefox wird noch nicht unterstützt, eine entsprechende Ausweitung ist aber geplant. Sie ist aber bereits in Safari verfügbar und wird in Chromium 105 veröffentlicht. Bei allen Demos in diesem Artikel wird angegeben, ob sie im verwendeten Browser nicht unterstützt werden.

Verwendung von :has

Wie sieht das aus? Betrachten Sie das folgende HTML-Beispiel mit zwei übergeordneten Elementen mit der Klasse everybody. Wie wählen Sie den Knoten aus, der einen Nachkommen mit der Klasse a-good-time hat?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

Mit :has() können Sie das mit dem folgenden CSS tun.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

Dadurch wird die erste Instanz von .everybody ausgewählt und ein animation angewendet.

In diesem Beispiel ist das Element mit der Klasse everybody das Ziel. Die Bedingung ist, dass ein Nachkomme mit der Klasse a-good-time vorhanden ist.

<target>:has(<condition>) { <styles> }

Sie können aber noch viel mehr damit machen, denn :has() bietet viele Möglichkeiten. Selbst solche, die wahrscheinlich noch nicht entdeckt wurden. Hier sind einige Beispiele:

Wählen Sie figure-Elemente mit einer direkten figcaption aus. css figure:has(> figcaption) { ... } anchors auswählen, die keinen direkten SVG-Abkömmling haben css a:not(:has(> svg)) { ... } labels auswählen, die ein direktes input-Schwesterelement haben Es geht seitwärts! css label:has(+ input) { … } articles auswählen, bei denen ein untergeordneter img keinen alt-Text enthält css article:has(img:not([alt])) { … } documentElement auswählen, bei dem ein bestimmter Status im DOM vorhanden ist css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Layoutcontainer mit einer ungeraden Anzahl von untergeordneten Elementen auswählen css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Alle Elemente in einem Raster auswählen, die nicht den Mauszeiger erhalten haben css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Container auswählen, der ein benutzerdefiniertes Element <todo-list> enthält css main:has(todo-list) { ... } Alle einzelnen a in einem Absatz auswählen, die ein direktes übergeordnetes hr-Element haben css p:has(+ hr) a:only-child { … } article auswählen, bei dem mehrere Bedingungen erfüllt sind css article:has(>h1):has(>h2) { … } Kombinieren Sie diese Anweisungen. Wähle einen article aus, wenn ein Titel von einem Untertitel gefolgt wird. css article:has(> h1 + h2) { … } Wähle den :root aus, wenn interaktive Status ausgelöst werden. css :root:has(a:hover) { … } Wähle den Absatz aus, der auf einen figure folgt, der keinen figcaption hat. css figure:not(:has(figcaption)) + p { … }

Welche interessanten Anwendungsfälle fallen Ihnen für :has() ein? Das Faszinierende daran ist, dass Sie dazu ermutigt werden, Ihr mentales Modell zu durchbrechen. Sie denken: „Könnte ich diese Stile anders angehen?“

Beispiele

Sehen wir uns einige Beispiele an, wie wir sie verwenden könnten.

Karten

Demo für klassische Karten ansehen Wir können beliebige Informationen auf unserer Karte anzeigen, z. B. einen Titel, eine Untertitelung oder Medien. Hier ist die Basiskarte.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

Was passiert, wenn Sie Medien einfügen möchten? Bei diesem Design könnte die Karte in zwei Spalten aufgeteilt werden. Zuvor können Sie eine neue Klasse erstellen, um dieses Verhalten darzustellen, z. B. card--with-media oder card--two-columns. Diese Klassennamen sind nicht nur schwer zu erfassen, sondern auch schwer zu verwalten und zu merken.

Mit :has() können Sie erkennen, dass sich Medien auf der Karte befinden, und entsprechend reagieren. Namen von Modifikatorklassen sind nicht erforderlich.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

Und Sie müssen es auch nicht dort lassen. Sie können dabei kreativ werden. Wie könnte sich eine Karte mit „Empfohlenen“ Inhalten in einem Layout anpassen? Mit diesem CSS-Code würde eine vorgestellte Karte die gesamte Breite des Layouts einnehmen und am Anfang eines Rasters platziert werden.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

Was passiert, wenn eine vorgestellte Karte mit einem Banner wackelt, um Aufmerksamkeit zu erregen?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

So viele Möglichkeiten.

Formulare

Wie sieht es mit Formularen aus? Sie sind dafür bekannt, dass sie schwierig zu stylen sind. Ein Beispiel hierfür sind Eingaben und ihre Labels. Wie signalisieren wir beispielsweise, dass ein Feld gültig ist? Mit :has() wird das viel einfacher. Wir können die entsprechenden Pseudoklassen für Formulare verwenden, z. B. :valid und :invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

Testen Sie das in diesem Beispiel: Geben Sie gültige und ungültige Werte ein und heben Sie den Fokus auf und wieder auf.

Mit :has() können Sie auch die Fehlermeldung für ein Feld ein- und ausblenden. Fügen Sie der Feldgruppe „E-Mail“ eine Fehlermeldung hinzu.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

Standardmäßig wird die Fehlermeldung ausgeblendet.

.form-group__error {
  display: none;
}

Wenn das Feld jedoch :invalid wird und nicht fokussiert ist, können Sie die Meldung ohne zusätzliche Klassennamen anzeigen.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

Es gibt keinen Grund, warum Sie nicht einen geschmackvollen Hauch von Verspieltheit hinzufügen könnten, wenn Nutzer mit Ihrem Formular interagieren. Betrachten Sie dieses Beispiel: Achten Sie darauf, dass Sie einen gültigen Wert für die Mikrointeraktion eingeben. Bei einem Wert von :invalid wackelt die Formulargruppe. Aber nur, wenn der Nutzer keine Bewegungseinstellungen hat.

Inhalt

Darauf sind wir bereits in den Codebeispielen eingegangen. Aber wie könnten Sie :has() in Ihrem Dokumentfluss verwenden? Es liefert Ideen dazu, wie wir die Typografie beispielsweise um Medien herum gestalten könnten.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

Dieses Beispiel enthält Zahlen. Wenn sie keine figcaption haben, schweben sie im Inhalt. Wenn ein figcaption vorhanden ist, nimmt es die gesamte Breite ein und erhält einen zusätzlichen Rand.

Auf Status reagieren

Wie wäre es, wenn Ihre Stile auf einen bestimmten Status in unserem Markup reagieren? Sehen wir uns ein Beispiel mit der „klassischen“ Navigationsleiste an. Wenn Sie eine Schaltfläche haben, mit der die Navigationsleiste geöffnet und geschlossen wird, wird möglicherweise das aria-expanded-Attribut verwendet. Die entsprechenden Attribute können mit JavaScript aktualisiert werden. Wenn aria-expanded = true ist, verwenden Sie :has(), um dies zu erkennen und die Stile für die Navigationsleiste zu aktualisieren. JavaScript erledigt seinen Teil und CSS kann mit diesen Informationen machen, was es will. Sie müssen das Markup nicht neu anordnen oder zusätzliche Klassennamen hinzufügen. Hinweis: Dies ist kein produktionsreifes Beispiel.

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

Kann :has helfen, Nutzerfehler zu vermeiden?

Was haben all diese Beispiele gemeinsam? Abgesehen davon, dass sie Möglichkeiten zur Verwendung von :has() zeigen, musste bei keiner der Lösungen der Klassenname geändert werden. Sie haben jeweils neue Inhalte eingefügt und ein Attribut aktualisiert. Dies ist ein großer Vorteil von :has(), da es dazu beitragen kann, Nutzerfehler zu vermeiden. Mit :has() kann CSS die Anpassung an Änderungen im DOM übernehmen. Sie müssen keine Klassennamen in JavaScript jonglieren, was das Risiko von Entwicklerfehlern verringert. Uns ist es schon allen passiert, dass wir einen Tippfehler in einen Klassennamen gemacht haben und ihn dann in Object-Suchanfragen behalten mussten.

Das ist ein interessanter Gedanke. Führt er zu einem saubereren Markup und weniger Code? Weniger JavaScript, da wir weniger JavaScript-Anpassungen vornehmen. Weniger HTML, da Klassen wie card card--has-media nicht mehr benötigt werden

Über den Tellerrand schauen

Wie bereits erwähnt, werden Sie bei :has() dazu angehalten, das mentale Modell zu durchbrechen. Es ist eine Gelegenheit, verschiedene Dinge auszuprobieren. Eine Möglichkeit, die Grenzen zu erweitern, besteht darin, Spielmechaniken nur mit CSS zu erstellen. Sie können beispielsweise eine schrittweise Mechanik mit Formularen und CSS erstellen.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

Das eröffnet interessante Möglichkeiten. So können Sie ein Formular mithilfe von Transformationen durchlaufen. Hinweis: Diese Demo lässt sich am besten in einem separaten Browsertab ansehen.

Und wie wäre es zum Spaß mit dem klassischen Buzz-Wire-Spiel? Mit :has() lässt sich die Mechanik einfacher erstellen. Wenn der Draht berührt wird, ist das Spiel vorbei. Ja, wir können einige dieser Spielmechaniken mithilfe von Elementen wie den Kombinatoren für Geschwisterelemente (+ und ~) erstellen. Mit :has() lassen sich aber dieselben Ergebnisse erzielen, ohne interessante Markup-„Tricks“ verwenden zu müssen. Hinweis: Diese Demo lässt sich am besten in einem separaten Browsertab ansehen.

Sie werden diese nicht so bald in die Produktion einbinden, aber sie zeigen Möglichkeiten auf, wie Sie das Primitive verwenden können. So können Sie beispielsweise eine :has() verketten.

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

Leistung und Einschränkungen

Was können Sie mit :has() nicht tun? Bei :has() gibt es einige Einschränkungen. Die Hauptprobleme sind Leistungseinbußen.

  • Sie können eine :has() nicht :has(). Sie können jedoch eine :has() verketten. css :has(.a:has(.b)) { … }
  • Keine Pseudoelemente innerhalb von :has() verwenden css :has(::after) { … } :has(::first-letter) { … }
  • Verwendung von :has() in Pseudos auf zusammengesetzte Auswahlkriterien beschränken css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Verwendung von :has() nach Pseudo-Element css ::part(foo):has(:focus) { … } einschränken
  • Die Verwendung von :visited ist immer falsch. css :has(:visited) { … }

Aktuelle Leistungsmesswerte zu :has() findest du in diesem Glitch. Vielen Dank an Byungwoo für diese Erkenntnisse und Details zur Implementierung.

Geschafft!

Mach dich bereit für :has(). Erzählt es euren Freunden und teilt diesen Beitrag. Das wird unsere Herangehensweise an CSS revolutionieren.

Alle Demos sind in dieser CodePen-Sammlung verfügbar.