:has(): il selettore della famiglia

Fin dall'inizio dei tempi (in termini CSS), abbiamo lavorato con una struttura a cascata in vari sensi. I nostri stili compongono un "Cascading Style Sheet". Anche i nostri selettori sono in cascata. Possono essere orizzontali. Nella maggior parte dei casi, si verificano in discesa. Ma mai verso l'alto. Da anni fantastichiamo su un "selettore di genitori". E ora finalmente è disponibile. Sotto forma di pseudo-selettore :has().

La pseudo-classe CSS :has() rappresenta un elemento se uno dei selettori passati come parametri corrisponde ad almeno un elemento.

Tuttavia, non si tratta solo di un selettore "genitore". È un bel modo di commercializzarlo. Il modo meno interessante potrebbe essere il selettore "ambiente condizionale". Ma non ha lo stesso suono. E il selettore "family"?

Supporto dei browser

Prima di procedere, vale la pena menzionare il supporto del browser. Non è ancora del tutto corretto. Ma è sempre più vicina. Non è ancora supportato su Firefox, ma è previsto in futuro. Tuttavia, è già disponibile in Safari e dovrebbe essere rilasciata in Chromium 105. Tutte le demo in questo articolo ti indicheranno se non sono supportate nel browser utilizzato.

Come utilizzare :has

Come si presenta? Prendi in considerazione il seguente codice HTML con due elementi fratelli con la classe everybody. Come selezioni quello che ha un discendente con la classe a-good-time?

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

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

Con :has(), puoi farlo con il seguente CSS.

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

Viene selezionata la prima istanza di .everybody e viene applicato un animation.

In questo esempio, l'elemento con la classe everybody è la destinazione. La condizione è avere un discendente con la classe a-good-time.

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

Ma puoi fare molto di più, perché :has() offre molte opportunità. Anche quelli che probabilmente non sono ancora stati scoperti. Considera alcune di queste.

Seleziona gli elementi figure che hanno un figcaption diretto. css figure:has(> figcaption) { ... } Seleziona i anchor che non hanno un discendente SVG diretto css a:not(:has(> svg)) { ... } Seleziona i label che hanno un elemento input fratello diretto. Spostamento laterale. css label:has(+ input) { … } Seleziona i article in cui un elemento discendente img non ha testo alt css article:has(img:not([alt])) { … } Seleziona l'elemento documentElement in cui è presente uno stato nel DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Seleziona il contenitore del layout con un numero dispari di elementi figli css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Seleziona tutti gli elementi di una griglia su cui non è presente il mouse css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Seleziona il contenitore che contiene un elemento personalizzato <todo-list> css main:has(todo-list) { ... } Seleziona ogni a singolo all'interno di un paragrafo che ha un elemento hr fratello diretto css p:has(+ hr) a:only-child { … } Seleziona un article in cui sono soddisfatte più condizioni css article:has(>h1):has(>h2) { … } Combina le opzioni. Seleziona un article quando un titolo è seguito da un sottotitolo css article:has(> h1 + h2) { … } Seleziona il :root quando vengono attivati gli stati interattivi css :root:has(a:hover) { … } Seleziona il paragrafo che segue un figure che non ha un figcaption css figure:not(:has(figcaption)) + p { … }

Quali casi d'uso interessanti ti vengono in mente per :has()? La cosa affascinante è che ti incoraggia a rompere il tuo modello mentale. Ti fa pensare: "Potrei approcciarmi a questi stili in modo diverso?".

Esempi

Vediamo alcuni esempi di come potremmo utilizzarlo.

Carte

Scatta una demo della scheda classica. Potremmo mostrare qualsiasi informazione nella nostra scheda, ad esempio un titolo, un sottotitolo o alcuni contenuti multimediali. Ecco la scheda di base.

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

Cosa succede quando vuoi inserire dei contenuti multimediali? Per questo design, la scheda potrebbe essere suddivisa in due colonne. In precedenza, potresti creare una nuova classe per rappresentare questo comportamento, ad esempio card--with-media o card--two-columns. Questi nomi di classe non solo diventano difficili da ricordare, ma anche da gestire e ricordare.

Con :has(), puoi rilevare che la scheda contiene alcuni contenuti multimediali ed eseguire l'azione appropriata. Non sono necessari nomi di classi di modificatori.

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

E non devi lasciarlo lì. Puoi scatenare la tua creatività. In che modo una scheda che mostra contenuti "in primo piano" può adattarsi a un layout? Questo CSS rende una scheda in primo piano della larghezza completa del layout e la posiziona all'inizio di una griglia.

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

Cosa succede se una scheda in primo piano con un banner si muove per attirare l'attenzione?

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

Infinite possibilità.

Moduli

E i moduli? Sono noti per essere difficili da acconciare. Un esempio è l'applicazione di stili agli input e alle relative etichette. Ad esempio, come facciamo a indicare che un campo è valido? Con :has(), è molto più facile. Possiamo collegarci alle pseudoclassi del modulo pertinenti, ad esempio :valid e :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);
}

Prova questo esempio: prova a inserire valori validi e non validi e attiva e disattiva lo stato attivo.

Puoi anche utilizzare :has() per mostrare e nascondere il messaggio di errore per un campo. Prendi il gruppo di campi "email" e aggiungi un messaggio di errore.

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

Per impostazione predefinita, il messaggio di errore viene nascosto.

.form-group__error {
  display: none;
}

Tuttavia, quando il campo diventa :invalid e non è attivo, puoi mostrare il messaggio senza la necessità di nomi di classi aggiuntivi.

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

Non c'è motivo per cui non aggiungere un tocco di fantasia quando gli utenti interagiscono con il tuo modulo. Considera questo esempio. Guarda quando inserisci un valore valido per la microinterazione. Un valore :invalid fa tremare il gruppo di moduli. Tuttavia, solo se l'utente non ha preferenze relative al movimento.

Contenuti

Ne abbiamo parlato negli esempi di codice. Ma come potresti utilizzare :has() nel flusso di documenti? Ad esempio, suggerisce idee su come dare stile alla tipografia nei contenuti multimediali.

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

Questo esempio contiene figure. Se non hanno figcaption, galleggiano all'interno dei contenuti. Quando è presente un figcaption, occupa l'intera larghezza e ottiene un margine aggiuntivo.

Reagire allo stato

Che ne dici di rendere i tuoi stili reattivi a uno stato nel nostro markup? Prendiamo ad esempio la barra di navigazione scorrevole "classica". Se hai un pulsante che attiva/disattiva l'apertura del menu di navigazione, potrebbe utilizzare l'attributo aria-expanded. JavaScript potrebbe essere utilizzato per aggiornare gli attributi appropriati. Quando aria-expanded è true, utilizza :has() per rilevarlo e aggiornare gli stili per il menu a scorrimento. JavaScript fa la sua parte e il CSS può fare ciò che vuole con queste informazioni. Non è necessario riorganizzare il markup o aggiungere altri nomi di classi e così via (nota: questo non è un esempio pronto per la produzione).

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

:has può aiutare a evitare errori da parte dell'utente?

Che cosa hanno in comune tutti questi esempi? A parte il fatto che mostrano modi per utilizzare :has(), nessuna di queste soluzioni ha richiesto la modifica dei nomi delle classi. Entrambi hanno inserito nuovi contenuti e aggiornato un attributo. Questo è un grande vantaggio di :has(), in quanto può contribuire ad attenuare gli errori dell'utente. Con :has(), il CSS è in grado di assumersi la responsabilità di adattarsi alle modifiche nel DOM. Non è necessario gestire i nomi delle classi in JavaScript, riducendo così le potenziali possibilità di errore dello sviluppatore. A tutti è capitato di commettere errori ortografici nei nomi delle classi e di dover ricorrere a mantenerli nelle ricerche Object.

È un pensiero interessante. Ci porta verso un markup più pulito e meno codice? Meno JavaScript perché non vengono apportati molti aggiustamenti. Meno codice HTML perché non avrai più bisogno di classi come card card--has-media e così via.

Pensare fuori dagli schemi

Come accennato sopra, :has() ti incoraggia a rompere il modello mentale. È un'opportunità per provare cose diverse. Un modo per provare a superare i limiti è creare le meccaniche di gioco solo con CSS. Ad esempio, puoi creare una procedura basata su passaggi con moduli e 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;
}

Il che apre interessanti possibilità. Puoi utilizzarlo per attraversare un modulo con trasformazioni. Tieni presente che questa demo è ottimale se visualizzata in una scheda del browser separata.

E per divertimento, che ne dici del classico gioco del filo spinato? La meccanica è più facile da creare con :has(). Se il cavo viene visualizzato, la partita è finita. Sì, possiamo creare alcune di queste meccaniche di gioco con elementi come i combinatori fratelli (+ e ~). Tuttavia, :has() è un modo per ottenere gli stessi risultati senza dover utilizzare "trucchi" di markup interessanti. Tieni presente che questa demo è ottimale se visualizzata in una scheda del browser separata.

Sebbene non sia possibile utilizzarle in produzione a breve, mettono in evidenza i modi in cui puoi utilizzare la primitiva. Ad esempio, la possibilità di concatenare un :has().

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

Prestazioni e limitazioni

Prima di concludere, cosa non puoi fare con :has()? Esistono alcune limitazioni per :has(). I principali si verificano a causa dei cali di rendimento.

  • Non puoi :has() un :has(). ma puoi concatenare un :has(). css :has(.a:has(.b)) { … }
  • Nessun utilizzo di pseudo elementi all'interno di :has() css :has(::after) { … } :has(::first-letter) { … }
  • Limitare l'uso di :has() all'interno di pseudo che accettano solo selettori composti css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Limitare l'utilizzo di :has() dopo lo pseudo elemento css ::part(foo):has(:focus) { … }
  • L'utilizzo di :visited sarà sempre false css :has(:visited) { … }

Per le metriche sul rendimento effettive relative a :has(), consulta questo glitch. Ringrazio Byungwoo per aver condiviso queste informazioni e dettagli sull'implementazione.

È tutto!

Preparati per :has(). Racconta di questa novità ai tuoi amici e condividi questo post: cambierà il modo in cui approcciamo i CSS.

Tutte le demo sono disponibili in questa raccolta di CodePen.