:has(): die Familienauswahl

Seit Beginn der Zeit (im Hinblick auf CSS) haben wir in verschiedenen Sinnbereichen mit einer Kaskade gearbeitet. Aus unseren Stilen ergibt sich ein „Cascading Style Sheet“. Auch unsere Selektoren lassen sich kaskadieren. Sie können seitlich gedreht werden. In den meisten Fällen gehen sie nach unten. Aber niemals höher. Wir denken schon seit Jahren über die Funktion „Auswahl für Eltern“. Und jetzt ist es endlich! Die Form eines :has()-Pseudoselektors.

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

Es ist jedoch mehr als eine „übergeordnete“ Auswahl. So kann man es gut vermarkten. Ein weniger ansprechender Weg könnte die Auswahl „bedingte Umgebung“ sein. Aber das hat nicht den gleichen Ring. Was ist mit der Auswahl „Familie“?

Unterstützte Browser

Bevor wir fortfahren, möchte ich Ihnen noch auf die Browserunterstützung hinweisen. Es ist noch nicht ganz am Ziel. Aber es rückt immer näher. Firefox wird derzeit noch nicht unterstützt, eine solche Funktion ist aber geplant. Der Browser ist jedoch bereits in Safari vorhanden und soll in Chromium 105 veröffentlicht werden. Die Demos in diesem Artikel zeigen Ihnen, ob sie vom verwendeten Browser nicht unterstützt werden.

Verwendung von :has

Wie sieht das aus? Betrachten Sie den folgenden HTML-Code mit zwei gleichgeordneten Elementen der Klasse everybody. Wie würden Sie diejenige auswählen, die 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 über den folgenden CSS-Code 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 einen Nachfolger mit der Klasse a-good-time.

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

Sie können aber noch viel weiter gehen, denn :has() bietet viele neue Möglichkeiten. Sogar solche, die wahrscheinlich noch nicht entdeckt wurden. Hier ein paar davon.

Wählen Sie figure-Elemente aus, die eine direkte figcaption haben. css figure:has(> figcaption) { ... } anchors ohne direktes untergeordnetes SVG-Element auswählen css a:not(:has(> svg)) { ... } Wählen Sie label-Elemente aus, die ein direktes Element input haben. Seitwärts! css label:has(+ input) { … } Wählen Sie articles aus, bei denen ein untergeordnetes Element (img) keinen alt-Text hat css article:has(img:not([alt])) { … } Wählen Sie documentElement aus, in dem 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)) { ... } Wählen Sie alle Elemente in einem Raster aus, die nicht über den Mauszeiger bewegt werden 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) { ... } Wählen Sie alle {12/ib/} in einem Absatz mit einer direkten article Bedingung aus, bei der {12/ib/} in einem Absatz eine direkte article Bedingung erfüllt hat:ahrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } article auswählen, bei dem ein Titel gefolgt von einem Untertitel ist css article:has(> h1 + h2) { … } Wähle die :root aus, wenn interaktive Status ausgelöst werden css :root:has(a:hover) { … } Wähle den Absatz aus, der auf eine figure folgt, der kein figcaption hat css figure:not(:has(figcaption)) + p { … }

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

Beispiele

Sehen wir uns einige Anwendungsbeispiele an.

Karten

Erstellen Sie eine Demo der klassischen Karte. Wir könnten auf unserer Karte beliebige Informationen anzeigen, z. B. einen Titel, eine Unterüberschrift oder bestimmte 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 einige Medien vorstellen? Für dieses Design könnte die Karte in zwei Spalten aufgeteilt werden. Zuvor konnten Sie eine neue Klasse erstellen, die dieses Verhalten darstellt, z. B. card--with-media oder card--two-columns. Es ist nicht nur schwierig, diese Klassennamen einzuschlagen, sondern auch schwer zu pflegen und zu merken.

Mit :has() können Sie erkennen, dass sich Medien auf der Karte befinden, und die entsprechende Aktion ausführen. Keine Modifikatorklassennamen 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 ihn nicht dort lassen. Lassen Sie Ihrer Kreativität freien Lauf. Wie könnte sich eine Karte mit „empfohlenen“ Inhalten an ein Layout anpassen? Mit diesem CSS wird eine hervorgehobene Karte über die volle Breite des Layouts hinweg dargestellt 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 passiert, wenn eine hervorgehobene Karte mit einem Banner nach Aufmerksamkeit wackelt?

<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 bekannt für ihre komplizierte Gestaltung. Ein Beispiel hierfür sind der Stil von Eingaben und deren Labels. Wie signalisieren wir, dass ein Feld z. B. gültig ist? Mit :has() wird das viel einfacher. Wir können Pseudoklassen in den relevanten Formularen einbinden, 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);
}

Probieren Sie es in diesem Beispiel aus: Versuchen Sie, gültige und ungültige Werte einzugeben und den Fokus ein- und auszuschalten.

Sie können auch :has() verwenden, um die Fehlermeldung für ein Feld ein- oder auszublenden. 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>

Die Fehlermeldung ist standardmäßig ausgeblendet.

.form-group__error {
  display: none;
}

Wenn das Feld jedoch zu :invalid wird und nicht fokussiert ist, können Sie die Nachricht ohne zusätzliche Kursnamen anzeigen lassen.

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

Die Interaktion der Nutzer mit dem Formular sollte eine geschmackvolle Note haben. Betrachten Sie dieses Beispiel. Ansehen, wenn Sie einen gültigen Wert für die Mikrointeraktion eingeben. Der Wert :invalid führt zu Erschütterungen der Formulargruppe. Aber nur, wenn Nutzende keine Bewegungspräferenzen haben.

Inhalte

Wir haben dies in den Codebeispielen bereits erwähnt. Aber wie können Sie :has() im Dokumentablauf verwenden? Wir entwickeln Ideen dazu, wie wir Typografie beispielsweise in Bezug auf 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 Abbildungen. Wenn sie keine figcaption haben, schweben sie im Inhalt. Wenn ein figcaption vorhanden ist, nehmen sie die volle Breite ein und erhalten einen zusätzlichen Rand.

Auf Status reagieren

Wie wäre es, Ihre Stile auf einen Zustand in unserem Markup reaktiv zu gestalten? Nehmen wir ein Beispiel mit der „klassischen“ verschiebbaren Navigationsleiste. Wenn Sie eine Schaltfläche zum Ein-/Ausblenden der Navigationsleiste haben, kann diese das Attribut aria-expanded verwenden. Die entsprechenden Attribute können mithilfe von JavaScript aktualisiert werden. Wenn aria-expanded den Wert true hat, verwenden Sie :has(), um dies zu erkennen und die Stile für die gleitende Navigationsleiste zu aktualisieren. JavaScript erledigt seinen Teil und CSS kann mit diesen Informationen tun, was es will. Es ist also nicht nötig, das Markup umzusortieren oder zusätzliche Kursnamen hinzuzufügen. Hinweis: Dies ist kein produktionsreifes Beispiel.

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

Kann :has dabei helfen, Nutzungsfehler zu vermeiden?

Was haben alle diese Beispiele gemeinsam? Abgesehen davon, dass sie Möglichkeiten zur Verwendung von :has() zeigen, musste keiner von ihnen Klassennamen ändern. Jeder hat neue Inhalte eingefügt und ein Attribut aktualisiert. Das ist ein großer Vorteil von :has(), da es dabei helfen kann, Nutzerfehler zu minimieren. Mit :has() kann CSS die Verantwortung für die Anpassung von Änderungen im DOM übernehmen. Sie müssen in JavaScript nicht zwischen Klassennamen hin- und herwechseln, wodurch das Risiko von Entwicklerfehlern geringer ist. Wir alle kennen das, wenn wir bei Klassennamen Tippfehlern gemacht haben und diese in Object-Suchen beibehalten müssen.

Das ist ein interessanter Gedanke und führt uns das zu einem saubereren Markup und weniger Code? Weniger JavaScript, da wir nicht so viele JavaScript-Anpassungen vornehmen. Weniger HTML, da Sie keine Klassen wie card card--has-media usw. mehr benötigen.

Über den Tellerrand blicken

Wie bereits erwähnt, ermutigen Sie :has(), das mentale Modell zu durchbrechen. Es ist eine Gelegenheit, verschiedene Dinge auszuprobieren. Eine Möglichkeit, die Grenzen zu überschreiten, besteht darin, Spielmechaniken allein mit CSS zu entwickeln. Sie könnten zum Beispiel einen schrittbasierten Mechanismus 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 uns interessante Möglichkeiten. Sie könnten dies verwenden, um eine Form mit Transformationen zu durchlaufen. Hinweis: Diese Demo wird am besten in einem separaten Browsertab angezeigt.

Und zum Spaß: Wie wäre es mit dem klassischen Bummel-Spiel? Dieser Vorgang ist mit :has() einfacher zu erstellen. Wenn der Mauszeiger darüber bewegt wird, ist das Spiel vorbei. Ja, wir können einige dieser Spielmechaniken mit Dingen wie den gleichgeordneten Kombinatoren (+ und ~) erstellen. Mit :has() lassen sich diese Ergebnisse jedoch auch erzielen, ohne auf interessante „Auszeichnungstricks“ zurückgreifen zu müssen. Hinweis: Diese Demo wird am besten in einem separaten Browsertab angezeigt.

Auch wenn Sie diese nicht so bald in die Produktion überführen werden, zeigen sie Ihnen, wie Sie die Primitive verwenden können. Ein Beispiel dafür wäre die Möglichkeit, ein :has() zu verketten.

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

Leistung und Einschränkungen

Zum Abschluss: Was kannst du mit :has() nicht tun? Für :has() gelten einige Einschränkungen. Die wichtigsten entstehen durch Leistungstreffer.

  • Sie können :has() nicht :has(). Du kannst aber :has() verketten. css :has(.a:has(.b)) { … }
  • Keine Pseudoelementnutzung in :has() css :has(::after) { … } :has(::first-letter) { … }
  • Verwendung von :has() innerhalb von Pseudonymen einschränken, bei denen nur zusammengesetzte Selektoren akzeptiert werden css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Verwendung von :has() nach dem Pseudoelement css ::part(foo):has(:focus) { … } einschränken
  • Die Verwendung von :visited ist immer falsch css :has(:visited) { … }

Tatsächliche Leistungsmesswerte im Zusammenhang mit :has() findest du in diesem Glitch. Vielen Dank an Byungwoo für dieses Feedback und die Details zur Implementierung.

Geschafft!

Bereit für :has()? Erzählen Sie Ihren Freunden davon und teilen Sie diesen Beitrag. Das wird sich in Bezug auf unsere Herangehensweise an CSS grundlegend verändern.

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