:has(): de familiekiezer

Sinds het begin van de tijd (in CSS-termen) hebben we in verschillende opzichten met een cascade gewerkt. Onze stijlen vormen een "Cascading Style Sheet". En onze selectors cascaderen ook. Ze kunnen zijwaarts gaan. In de meeste gevallen gaan ze naar beneden. Maar nooit naar boven. Wij fantaseren al jaren over een ‘Ouderkiezer’. En nu komt het eindelijk! In de vorm van een :has() pseudo-selector.

De CSS-pseudoklasse :has() vertegenwoordigt een element als een van de als parameters doorgegeven selectors overeenkomt met ten minste één element.

Maar het is meer dan een "bovenliggende" selector. Dat is een leuke manier om het op de markt te brengen. De niet zo aantrekkelijke manier zou de selector "voorwaardelijke omgeving" kunnen zijn. Maar dat heeft niet helemaal dezelfde klank. Hoe zit het met de 'familie'-selector?

Browserondersteuning

Voordat we verder gaan, is het de moeite waard om browserondersteuning te vermelden. Het is er nog niet helemaal. Maar het komt dichterbij. Nog geen Firefox-ondersteuning, het staat op de routekaart. Maar het is al beschikbaar in Safari en zal verschijnen in Chromium 105. Alle demo's in dit artikel zullen u vertellen of ze niet worden ondersteund in de gebruikte browser.

Hoe te gebruiken: heeft

Dus hoe ziet het eruit? Beschouw de volgende HTML met twee broers en zussen met de klasse everybody . Hoe zou je degene selecteren die een afstammeling heeft met de klasse a-good-time ?

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

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

Met :has() kun je dat doen met de volgende CSS.

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

Hiermee wordt de eerste instantie van .everybody geselecteerd en wordt een animation toegepast.

In dit voorbeeld is het element met de klasse everybody het doel. Voorwaarde is dat een nakomeling in de klasse a-good-time heeft.

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

Maar je kunt nog veel verder gaan, want :has() biedt veel mogelijkheden. Zelfs degenen die waarschijnlijk nog niet ontdekt zijn. Overweeg enkele hiervan.

Selecteer figure die een direct figcaption hebben. css figure:has(> figcaption) { ... } Selecteer anchor s die geen directe SVG-afstammeling hebben css a:not(:has(> svg)) { ... } Selecteer label -s die een directe input hebben broer of zus. Zijwaarts gaan! css label:has(+ input) { … } Selecteer article s waar een afstammeling img geen alt tekst heeft css article:has(img:not([alt])) { … } Selecteer het documentElement waarin een bepaalde status aanwezig is de DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Selecteer de lay-outcontainer met een oneven aantal kinderen css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Selecteer alle items in een raster waar geen zweeftekst op css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Selecteer de container die een aangepast element bevat <todo-list> css main:has(todo-list) { ... } Selecteer elke solo a binnen een paragraaf die een direct hr heeft css p:has(+ hr) a:only-child { … } Selecteer een article waarbij aan meerdere voorwaarden wordt voldaan css article:has(>h1):has(>h2) { … } Meng dat door elkaar. Selecteer een article waarbij een titel wordt gevolgd door een ondertitel css article:has(> h1 + h2) { … } Selecteer de :root wanneer interactieve toestanden worden geactiveerd css :root:has(a:hover) { … } Selecteer de paragraaf die volgt een figure die geen figcaption css figure:not(:has(figcaption)) + p { … }

Welke interessante gebruiksscenario's kun je bedenken voor :has() ? Het fascinerende hier is dat het je aanmoedigt om je mentale model te doorbreken. Het doet je denken: "Kan ik deze stijlen op een andere manier benaderen?".

Voorbeelden

Laten we enkele voorbeelden bekijken van hoe we het kunnen gebruiken.

Kaarten

Neem een ​​klassieke kaartdemo. We kunnen alle informatie op onze kaart weergeven, bijvoorbeeld: een titel, ondertitel of bepaalde media. Hier is de basiskaart.

<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>

Wat gebeurt er als je bepaalde media wilt introduceren? Voor dit ontwerp kon de kaart in twee kolommen worden gesplitst. Voorheen kon u een nieuwe klasse maken om dit gedrag weer te geven, bijvoorbeeld card--with-media card--two-columns . Deze klassenamen worden niet alleen moeilijk te bedenken, maar ook moeilijk te onderhouden en te onthouden.

Met :has() kunt u detecteren dat de kaart over bepaalde media beschikt en het juiste doen. Er zijn geen modifier-klassenamen nodig.

<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>

En je hoeft het daar niet te laten. Je zou er creatief mee aan de slag kunnen gaan. Hoe kan een kaart met ‘uitgelichte’ inhoud zich aanpassen binnen een lay-out? Deze CSS zou een uitgelichte kaart over de volledige breedte van de lay-out maken en deze aan het begin van een raster plaatsen.

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

Wat als een uitgelichte kaart met een banner heen en weer wiebelt om aandacht?

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

Zoveel mogelijkheden.

Formulieren

Hoe zit het met formulieren? Ze staan ​​bekend als lastig te stylen. Een voorbeeld hiervan is het stylen van invoer en hun labels. Hoe geven we bijvoorbeeld aan dat een veld geldig is? Met :has() wordt dit veel eenvoudiger. We kunnen pseudo-klassen in de relevante vorm inhaken, bijvoorbeeld :valid en :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);
}

Probeer het eens in dit voorbeeld: probeer geldige en ongeldige waarden in te voeren en de focus aan en uit te zetten.

U kunt ook :has() gebruiken om de foutmelding voor een veld weer te geven of te verbergen. Neem onze veldgroep 'e-mail' en voeg er een foutmelding aan toe.

<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>

Standaard verbergt u de foutmelding.

.form-group__error {
  display: none;
}

Maar wanneer het veld :invalid wordt en geen focus heeft, kunt u het bericht weergeven zonder dat er extra klassennamen nodig zijn.

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

Er is geen reden waarom u niet een smaakvol vleugje eigenzinnigheid zou kunnen toevoegen aan de interactie van uw gebruikers met uw formulier. Denk eens aan dit voorbeeld. Let op wanneer u een geldige waarde invoert voor de micro-interactie. Een :invalid waarde zorgt ervoor dat de formuliergroep schudt. Maar alleen als de gebruiker geen bewegingsvoorkeuren heeft.

Inhoud

We hebben dit besproken in de codevoorbeelden. Maar hoe kunt u :has() in uw documentstroom gebruiken? Het levert ideeën op over hoe we typografie bijvoorbeeld rond media kunnen vormgeven.

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

Dit voorbeeld bevat cijfers. Als ze geen figcaption hebben, zweven ze binnen de inhoud. Wanneer er een figcaption aanwezig is, nemen ze de volledige breedte in beslag en krijgen ze extra marge.

Reageren op de staat

Hoe zit het met het reactief maken van uw stijlen op een bepaalde status in onze opmaak. Beschouw een voorbeeld met de "klassieke" verschuifbare navigatiebalk. Als u een knop heeft waarmee u de navigatie kunt openen, kan deze het aria-expanded attribuut gebruiken. JavaScript kan worden gebruikt om de juiste kenmerken bij te werken. Wanneer aria-expanded true is, gebruik :has() om dit te detecteren en de stijlen voor het schuifnavigatiesysteem bij te werken. JavaScript doet zijn deel en CSS kan met die informatie doen wat het wil. Het is niet nodig om de opmaak in willekeurige volgorde te verplaatsen of extra klassennamen toe te voegen, enz. (Opmerking: dit is geen productieklaar voorbeeld).

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

Kan :heeft helpen gebruikersfouten te voorkomen?

Wat hebben al deze voorbeelden gemeen? Afgezien van het feit dat ze manieren laten zien om :has() te gebruiken, vereiste geen van deze het wijzigen van klassenamen. Ze hebben elk nieuwe inhoud ingevoegd en een kenmerk bijgewerkt. Dit is een groot voordeel van :has() , omdat het gebruikersfouten kan helpen verminderen. Met :has() kan CSS de verantwoordelijkheid op zich nemen voor aanpassingen aan wijzigingen in de DOM. U hoeft in JavaScript niet met klassenamen te jongleren, waardoor er minder kans is op ontwikkelaarsfouten. We zijn er allemaal wel eens geweest als we een klassenaam typten en onze toevlucht moesten nemen tot het bewaren ervan in Object zoekopdrachten.

Het is een interessante gedachte en leidt het ons naar schonere markup en minder code? Minder JavaScript omdat we niet zoveel JavaScript-aanpassingen doorvoeren. Minder HTML omdat je geen klassen meer nodig hebt zoals card card--has-media , enz.

outside-the-box denken

Zoals hierboven vermeld, moedigt :has() je aan om het mentale model te doorbreken. Het is een kans om verschillende dingen te proberen. Eén manier om de grenzen te verleggen is door spelmechanismen alleen met CSS te maken. U kunt bijvoorbeeld een op stappen gebaseerd mechanisme maken met formulieren en CSS.

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

En dat biedt interessante mogelijkheden. Je zou dat kunnen gebruiken om een ​​formulier met transformaties te doorkruisen. Let op: deze demo kan het beste worden bekeken in een apart browsertabblad.

En voor de lol: wat dacht je van het klassieke Buzz Wire-spel? Het mechanisme is eenvoudiger te maken met :has() . Als er over de draad zweeft, is het spel afgelopen. Ja, we kunnen een aantal van deze spelmechanismen creëren met zaken als de broer/zus -combinatoren ( + en ~ ). Maar :has() is een manier om dezelfde resultaten te bereiken zonder interessante opmaaktrucs te hoeven gebruiken. Let op: deze demo kan het beste worden bekeken in een apart browsertabblad.

Hoewel je deze niet snel in productie zult nemen, benadrukken ze manieren waarop je de primitieve kunt gebruiken. Zoals het kunnen ketenen van een :has() .

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

Prestaties en beperkingen

Voordat we gaan, wat kun je niet doen met :has() ? Er zijn enkele beperkingen met :has() . De belangrijkste ontstaan ​​door de prestatiehits.

  • Je kunt niet :has() een :has() . Maar je kunt een :has() ketenen. css :has(.a:has(.b)) { … }
  • Geen gebruik van pseudo-elementen binnen :has() css :has(::after) { … } :has(::first-letter) { … }
  • Beperk het gebruik van :has() binnen pseudos en accepteer alleen samengestelde selectors css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Beperk het gebruik van :has() na pseudo-element css ::part(foo):has(:focus) { … }
  • Gebruik van :visited zal altijd false zijn css :has(:visited) { … }

Voor actuele prestatiestatistieken gerelateerd aan :has() , bekijk deze Glitch . Met dank aan Byungwoo voor het delen van deze inzichten en details rond de implementatie.

Dat is het!

Maak je klaar voor :has() . Vertel je vrienden erover en deel dit bericht. Het zal een gamechanger zijn voor de manier waarop we CSS benaderen.

Alle demo's zijn beschikbaar in deze CodePen-collectie .