:has(): die Familienauswahl

Seit Beginn (in Bezug auf CSS) haben wir in verschiedenen Bereichen mit einer Kaskade gearbeitet. Unsere Stile ergeben ein sogenanntes Cascading Style Sheet. Und unsere Selektoren kaskaden. Sie können seitlich gehen. In den meisten Fällen gehen sie nach unten. Aber nie nach oben. Wir denken schon seit Jahren über einen „Parent Selector“. Und jetzt ist es endlich so weit! In Form eines :has()-Pseudoselektors.

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

Aber es ist mehr als nur ein Elternteil Selektor. Das ist eine schöne Möglichkeit, es zu vermarkten. Ein weniger ansprechender Weg ist vielleicht die „bedingte Umgebung“, Selektor. Aber das muss nicht so klingen. Wie wäre es mit der „Familie“? Selektor?

Unterstützte Browser

Bevor wir fortfahren, möchte ich noch auf die Browserunterstützung eingehen. Es ist noch nicht ganz so weit. Aber es rückt immer näher. Firefox wird noch nicht unterstützt, dies ist jedoch in Planung. Sie ist aber bereits in Safari enthalten und wird in Chromium 105 veröffentlicht. In allen Demos in diesem Artikel erfahren Sie, falls sie von dem verwendeten Browser nicht unterstützt werden.

Verwendung von :has

Wie sieht das aus? Sehen Sie sich den folgenden HTML-Code mit zwei gleichgeordneten Elementen der Klasse everybody an. Wie würden Sie das Element auswählen, das ein Nachfolgerelement 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 dies 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 hat ein Nachfolgerelement mit der Klasse a-good-time.

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

Sie können aber noch viel weiter gehen, denn :has() bietet viele Möglichkeiten. Nicht einmal solche, die wahrscheinlich noch nicht entdeckt wurden. Sehen Sie sich einige davon an.

Wählen Sie figure-Elemente mit einem direkten figcaption aus. css figure:has(> figcaption) { ... } anchor-Elemente auswählen, die keinen direkten SVG-Nachfolger haben css a:not(:has(> svg)) { ... } Wählen Sie label-Elemente aus, die einen direkten input-Geschwister haben. Seitwärts! css label:has(+ input) { … } Wählen Sie article-Elemente aus, in denen ein untergeordnetes Element von img keinen alt-Text enthält css article:has(img:not([alt])) { … } Wählen Sie die documentElement aus, bei der ein Status im DOM vorhanden ist css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Wählen Sie den Layoutcontainer mit einer ungeraden Anzahl von untergeordneten Elementen aus css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Alle Elemente in einem Raster auswählen, auf die sich nichts bewegt css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Wählen Sie den Container aus, der ein benutzerdefiniertes Element enthält <todo-list> css main:has(todo-list) { ... } Jedes Solo a innerhalb eines Absatzes auswählen, das ein direktes gleichgeordnetes hr-Element hat css p:has(+ hr) a:only-child { … } Wählen Sie eine article aus, bei der mehrere Bedingungen erfüllt sind css article:has(>h1):has(>h2) { … } Hier findest du einen Mix. Wähle einen article aus, in dem auf einen Titel ein Untertitel folgt css article:has(> h1 + h2) { … } :root auswählen, wenn interaktive Status ausgelöst werden css :root:has(a:hover) { … } Wählen Sie den Absatz aus, der auf figure folgt und kein figcaption-Zeichen enthält. css figure:not(:has(figcaption)) + p { … }

Welche interessanten Anwendungsfälle fallen Ihnen für :has() ein? Das Interessante dabei ist, dass Sie dazu ermutigt werden, Ihr Denkmodell zu brechen. Da stellt man sich die Frage: „Könnte ich diese Stile auf eine andere Art angehen?“

Beispiele

Sehen wir uns einige Beispiele an.

Karten

Demo zur klassischen Karte ansehen Wir könnten alle Informationen auf unserer Karte anzeigen, zum Beispiel einen Titel, eine Unterüberschrift oder einige 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ühren möchten? Bei diesem Design könnte die Karte in zwei Spalten aufgeteilt werden. Zuvor können Sie eine neue Klasse erstellen, die dieses Verhalten darstellt, z. B. card--with-media oder card--two-columns. Diese Klassennamen werden nicht nur schwer zu beschwören, sondern auch schwer zu verwalten und zu merken.

Mit :has() kannst du erkennen, dass die Karte einige Medien enthält, und die entsprechenden Aktionen ausführen. Es sind keine Namen für Modifikatorklassen 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 nicht dort lassen. Sie könnten kreativ werden. Wie könnte sich eine Karte mit „hervorgehobenen“ Inhalten an ein Layout anpassen? Mit diesem CSS wird eine hervorgehobene Karte über die gesamte Breite des Layouts erstellt und am Anfang eines Rasters platziert.

.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 ist, wenn eine empfohlene 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 knifflig sind. Ein solches Beispiel sind Stileingaben und ihre Labels. Wie signalisieren wir beispielsweise, dass ein Feld gültig ist? Mit :has() geht das viel einfacher. Wir können in Pseudoklassen wie :valid und :invalid einsteigen.

<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);
}

Probieren Sie es in diesem Beispiel aus: Geben Sie gültige und ungültige Werte ein und legen Sie den Fokus nach und nach ab.

Sie können auch :has() verwenden, um die Fehlermeldung für ein Feld ein- oder auszublenden. Fügen Sie der Feldgruppe „email“ (E-Mail-Adresse) 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 ist die Fehlermeldung ausgeblendet.

.form-group__error {
  display: none;
}

Wenn das Feld jedoch zu :invalid wird und nicht hervorgehoben ist, können Sie die Nachricht anzeigen, ohne zusätzliche Klassennamen zu benötigen.

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

Es gibt keinen Grund, den Nutzern bei der Interaktion mit dem Formular eine geschmackvolle Note zu geben. Betrachten Sie dieses Beispiel: Beobachten Sie, ob Sie einen gültigen Wert für die Mikrointeraktion eingeben. Ein :invalid-Wert bewirkt, dass die Formulargruppe erschüttert wird. Aber nur, wenn Nutzende keine Bewegungspräferenzen haben.

Inhalt

Wir haben dies in den Codebeispielen angesprochen. Aber wie können Sie :has() in Ihrem Dokumentfluss verwenden? Sie liefert uns Ideen dazu, wie wir beispielsweise die Typografie für Medien 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 kein figcaption haben, schweben sie im Inhalt. Wenn figcaption vorhanden ist, nehmen sie die volle Breite ein und erhalten einen zusätzlichen Rand.

Reaktion auf Zustand

Sie könnten Ihre Stile auf einen bestimmten Zustand in unserem Markup reagieren. Ein Beispiel mit dem „klassischen“ verschiebbare Navigationsleiste. Wenn Sie eine Schaltfläche haben, mit der sich die Navigation umschalten lässt, kann das Attribut aria-expanded verwendet werden. Die entsprechenden Attribute können mithilfe von JavaScript aktualisiert werden. Wenn aria-expanded den Wert true hat, verwenden Sie :has(), um dies zu ermitteln und die Stile für die gleitende Navigation zu aktualisieren. JavaScript erledigt diese Aufgabe und CSS kann mit diesen Informationen tun, was es will. Das Markup muss nicht umsortiert oder zusätzliche Klassennamen usw. hinzugefügt werden. (Hinweis: Dieses Beispiel ist nicht für die Produktion geeignet.)

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

Kann :has dazu beitragen, Nutzerfehler zu vermeiden?

Was haben alle diese Beispiele gemeinsam? Sie zeigen zwar, wie man :has() verwendet, aber keiner von ihnen musste Klassennamen ändern. Beide haben neue Inhalte eingefügt und ein Attribut aktualisiert. Dies ist ein großer Vorteil von :has(), da damit Nutzerfehler reduziert werden können. Mit :has() kann das CSS die Verantwortung für die Anpassung an Änderungen im DOM übernehmen. Sie müssen Klassennamen in JavaScript nicht ändern, wodurch das Risiko von Entwicklerfehlern verringert wird. Wir alle kennen das, wenn wir einen Klassennamen vertippen, und müssen ihn bei Object-Suchen beibehalten.

Dies ist ein interessanter Gedanke und führt uns zu saubererem Markup und weniger Code? Weniger JavaScript, da wir weniger JavaScript-Anpassungen vornehmen. Weniger HTML-Code, da Sie keine Klassen wie card card--has-media mehr benötigen

Um die Ecke denken

Wie bereits erwähnt, ermutigt dich :has(), das mentale Modell zu brechen. Es ist eine Gelegenheit, etwas Neues auszuprobieren. Eine Möglichkeit, die Grenzen zu überschreiten, besteht darin, nur mit CSS Spielmechaniken zu entwickeln. Sie könnten beispielsweise eine schrittbasierte 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. Damit könnten Sie ein Formular mit Transformationen durchlaufen. Hinweis: Diese Demo wird am besten in einem separaten Browsertab angezeigt.

Und zum Spaß: Wie wäre es mit dem klassischen Buzz-Wire-Spiel? Die Mechanik lässt sich mit :has() einfacher erstellen. Wenn du den Mauszeiger darüber bewegst, ist das Spiel beendet. Ja, mithilfe von Kombinatoren (+ und ~) können wir einige dieser Spielmechaniken erstellen. Mit :has() erreichen Sie jedoch dieselben Ergebnisse, ohne dass Sie interessante Markup-"Tricks" verwenden müssen. Hinweis: Diese Demo wird am besten in einem separaten Browsertab angezeigt.

Auch wenn Sie diese nicht in Kürze in die Produktion nehmen werden, zeigen sie Möglichkeiten auf, wie Sie die Primitive verwenden können. Beispielsweise kann ein :has() verkettet werden.

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

Leistung und Einschränkungen

Was kannst du nicht mit :has() machen? Bei :has() gelten einige Einschränkungen. Die wichtigsten entstehen durch Leistungssteigerungen.

  • Sie können :has() nicht :has(). Sie können aber ein :has() verketten. css :has(.a:has(.b)) { … }
  • Keine Verwendung eines Pseudoelements innerhalb von :has() css :has(::after) { … } :has(::first-letter) { … }
  • Verwendung von :has() in Pseudoen beschränken, die nur zusammengesetzte Selektoren akzeptieren css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Verwendung von :has() nach Pseudoelement einschränken css ::part(foo):has(:focus) { … }
  • Die Verwendung von :visited ist immer falsch css :has(:visited) { … }

Informationen zu tatsächlichen Leistungsmesswerten für :has() findest du in dieser Störung. Danke an Byungwoo für das Teilen dieser Erkenntnisse und Details zur Implementierung.

Geschafft!

Mach dich bereit für :has(). Erzählen Sie Ihren Freunden davon und teilen Sie diesen Beitrag mit uns. Er wird die Art und Weise, wie wir CSS angehen, grundlegend verändern.

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