:has(): il selettore della famiglia

Sin dall'inizio (in termini CSS), abbiamo lavorato a una cascata in vari sensi. I nostri stili formano un "Foglio di stile a cascata". E anche i nostri selettori cadono in cascata. Possono andare lateralmente. Nella maggior parte dei casi scendono. Ma mai verso l'alto. Per anni abbiamo fantasticato su un "Selettore genitori". E ora sta arrivando! A forma di pseudoselettore :has().

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

Ma è molto più di un "genitore" selettore. È un bel modo di promuoverlo. Un modo meno interessante potrebbe essere l'"ambiente condizionale" selettore. Ma questo non ha lo stesso anello. La "famiglia" selettore?

Supporto dei browser

Prima di proseguire, vale la pena menzionare il supporto dei browser. Non ha ancora finito. Ma ci stiamo avvicinando. Firefox non è ancora supportato, ma è in programma. Ma è già in Safari e dovrebbe essere rilasciato in Chromium 105. Tutte le demo riportate in questo articolo indicano se non sono supportate nel browser utilizzato.

Come utilizzare :has

Come si presenta? Considera il seguente codice HTML con due elementi di pari livello con la classe everybody. Come selezioneresti l'oggetto 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 è il target. La condizione ha un discendente con la classe a-good-time.

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

Ma non è tutto, perché :has() offre molte opportunità. Anche quelli probabilmente non ancora scoperti. Prendi in considerazione alcuni di questi.

Seleziona gli elementi figure che hanno un figcaption diretto. css figure:has(> figcaption) { ... } Seleziona anchor che non hanno un discendente diretto in formato SVG css a:not(:has(> svg)) { ... } Seleziona label con un fratello input diretto. Andiamo lateralmente! css label:has(+ input) { … } Seleziona article in cui un img discendente non ha testo alt css article:has(img:not([alt])) { … } Seleziona documentElement in cui è presente uno stato nel DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Seleziona il contenitore di layout con un numero dispari di elementi secondari css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Seleziona tutti gli elementi di una griglia su cui non è stato eseguito il passaggio del 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 assolo a all'interno di un paragrafo che ha un elemento di pari livello hr diretto css p:has(+ hr) a:only-child { … } Seleziona un article in cui sono soddisfatte più condizioni css article:has(>h1):has(>h2) { … } Fai un bel mix. Seleziona un article in cui un titolo sia seguito da un sottotitolo css article:has(> h1 + h2) { … } Seleziona :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 qui è che ti ha incoraggiato a rompere il tuo modello mentale. Ti viene in mente: "Posso affrontare questi stili in un modo diverso?".

Esempi

Vediamo alcuni esempi di utilizzo.

Carte

Prova una demo classica della carta. 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 presentare alcuni contenuti multimediali? In questo caso, la scheda potrebbe essere suddivisa in due colonne. In precedenza, puoi creare una nuova classe per rappresentare questo comportamento, ad esempio card--with-media o card--two-columns. I nomi di questi corsi non solo diventano difficili da evocare, ma diventano anche difficili da mantenere e ricordare.

Con :has(), puoi rilevare che la scheda contiene alcuni contenuti multimediali e compiere l'azione appropriata. Non sono necessari nomi delle 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 c'è bisogno di lasciarlo lì. Potresti dare sfogo alla tua creatività. In che modo una scheda che mostra contenuti "in primo piano" potrebbe adattarsi all'interno di un layout? Questo CSS crea una scheda in primo piano per l'intera larghezza 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);
}

E se una scheda in primo piano con un banner oscilli 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;
}

Tante possibilità.

Moduli

E i moduli? Sono noti per essere difficili da stile. Un esempio è l'applicazione di stili agli input e alle relative etichette. Ad esempio, come segnaliamo che un campo è valido? Con :has(), è molto più facile. Possiamo agganciarci alle pseudo-classi 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 a usare questo esempio: prova a inserire valori validi e non validi e ad attivare e disattivare lo stato attivo.

Puoi anche utilizzare :has() per mostrare e nascondere il messaggio di errore di un campo. Aggiungi un messaggio di errore al gruppo di campi "Email".

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

Ma quando il campo diventa :invalid e non è attivo, puoi mostrare il messaggio senza dover utilizzare nomi di classi aggiuntivi.

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

Non c'è motivo di non poter aggiungere un pizzico di stravaganza per quando gli utenti interagiscono con il modulo. Considera questo esempio. Controlla quando inserisci un valore valido per la micro-interazione. Un valore :invalid causerà lo scuotimento del gruppo di moduli. ma solo se l'utente non ha preferenze di movimento.

Contenuti

Ne abbiamo parlato negli esempi di codice. Ma come potresti utilizzare :has() nel flusso di documenti? Ad esempio, fa emergere idee su come definire lo stile della tipografia per i media.

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, fluttuano all'interno dei contenuti. Quando è presente un elemento figcaption, occupa l'intera larghezza e ottiene un margine aggiuntivo.

Reazione allo stato

Che ne dici di rendere i tuoi stili reattivi a qualche stato nel nostro markup? Considera un esempio con il modello "classico" barra di navigazione scorrevole. Se hai un pulsante che attiva/disattiva l'apertura del menu di navigazione, potrebbe usare l'attributo aria-expanded. JavaScript può essere utilizzato per aggiornare gli attributi appropriati. Quando il criterio aria-expanded è impostato su true, usa :has() per rilevarlo e aggiornare gli stili della navigazione scorrevole. JavaScript fa la sua parte e il CSS può fare ciò che vuole con queste informazioni. Non è necessario mischiare 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));
}

Può :ha aiuto per evitare errori dell'utente?

Cosa hanno in comune tutti questi esempi? A parte il fatto che mostrano modi per utilizzare :has(), nessuno di loro richiede di modificare i nomi delle classi. Ognuna ha inserito nuovi contenuti e aggiornato un attributo. Si tratta di un grande vantaggio di :has(), in quanto può contribuire a mitigare l'errore dell'utente. Con :has(), il CSS è in grado di assumersi la responsabilità di adattarsi alle modifiche nel DOM. Non è necessario destreggiarsi tra i nomi delle classi in JavaScript, il che riduce il rischio di errori dello sviluppatore. Ci siamo tutti quando abbiamo sbagliato a digitare il nome di un corso e dobbiamo ricorrere a questa operazione in Object ricerche.

È un pensiero interessante e ci porta a un markup più chiaro e a meno codice? Meno JavaScript perché non stiamo apportando molte modifiche JavaScript. Meno HTML in quanto non hai più bisogno di classi come card card--has-media e così via.

Pensare fuori dagli schemi

Come accennato in precedenza, il :has() incoraggia a rompere il modello mentale. È l'occasione per provare cose diverse. Un modo per cercare di superare i limiti è creare meccaniche di gioco solo con CSS. Potresti creare una meccanica basata su passaggi, ad esempio 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;
}

E questo apre possibilità interessanti. Potresti usarlo per attraversare un modulo con le trasformazioni. Tieni presente che questa demo viene visualizzata in modo ottimale in una scheda del browser separata.

E per divertimento, che ne dici del classico gioco del passaparola? È più facile creare il meccanico con :has(). Passando il mouse sopra il cavo, la partita è finita. Sì, possiamo creare alcune di queste meccaniche di gioco con elementi come i combinatori di pari livello (+ e ~). Tuttavia, :has() è un modo per ottenere gli stessi risultati senza dover utilizzare interessanti "trucchi di markup". Tieni presente che questa demo viene visualizzata in modo ottimale in una scheda del browser separata.

Anche se non la metterai in produzione a breve, evidenziano alcuni modi in cui puoi utilizzare la primitiva. come 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 andare, cosa non puoi fare con :has()? Esistono alcune limitazioni relative a :has(). I principali nascono dalle hit del rendimento.

  • Non puoi :has() a :has(). Ma puoi concatenare un :has(). css :has(.a:has(.b)) { … }
  • Nessun utilizzo di pseudoelemento in :has() css :has(::after) { … } :has(::first-letter) { … }
  • Limita 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)) { … }
  • Limita l'utilizzo di :has() dopo lo pseudoelemento css ::part(foo):has(:focus) { … }
  • L'utilizzo di :visited sarà sempre falso css :has(:visited) { … }

Per conoscere le metriche sul rendimento effettive relative a :has(), consulta questo Glitch. Ringraziamo Byungwoo per aver condiviso questi approfondimenti e dettagli sull'implementazione.

È tutto!

Preparati a :has(). Raccontalo ai tuoi amici e condividi questo post: sarà una svolta per il nostro approccio ai CSS.

Tutte le demo sono disponibili in questa collezione CodePen.