:has(): il selettore della famiglia

Sin dall'inizio (in termini CSS), lavoriamo a cascata in vari sensi. I nostri stili compongono un "Cascading Style Sheet". E anche i nostri selettori si attivano a cascata. Possono essere ruotate. Nella maggior parte dei casi, i valori sono scesi. Ma mai verso l'alto. Per anni abbiamo fantasticato su un "Selettore genitore". E ora è in arrivo! A forma di pseudo-selettore :has().

La pseudo-classe CSS :has() rappresenta un elemento se uno o più selettori trasmessi come parametri corrisponde ad almeno un elemento.

Ma è più di un selettore "padre". È un bel modo per commercializzarlo. A volte non è così accattivante il selettore dell'"ambiente condizionale". Ma non ha lo stesso suono. Che ne dici del selettore "famiglia"?

Supporto dei browser

Prima di proseguire, vale la pena menzionare il supporto dei browser. Non è ancora lì. Ma ci si avvicina sempre di più. Firefox non supporta ancora, ma è in arrivo. Tuttavia è già in Safari e dovrebbe essere rilasciato in Chromium 105. Tutte le demo in questo articolo ti indicheranno se non sono supportate nel browser utilizzato.

Come utilizzare :ha

Come si presenta? Considera il seguente codice HTML con due elementi di pari livello con la classe everybody. Come selezioneresti quella con un discendente con la classe a-good-time?

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

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

Puoi farlo con :has() 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 puoi andare oltre, perché :has() ti offre molte opportunità. Anche quelli probabilmente non ancora scoperti. Prendi in considerazione alcuni di questi.

Seleziona elementi figure che hanno un valore figcaption diretto. css figure:has(> figcaption) { ... } Seleziona anchor che non hanno un discendente SVG diretto css a:not(:has(> svg)) { ... } Seleziona label che hanno un fratello input diretto. Stai andando di lato! css label:has(+ input) { … } Seleziona article dove un discendente img non ha testo alt css article:has(img:not([alt])) { … } Seleziona documentElement dove è presente uno stato nel DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Seleziona il contenitore del 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 a cui non è stato passato 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 singolo elemento a all'interno di un paragrafo con un elemento di pari /2 diretto.articlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Seleziona un article in cui un titolo è 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 una 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 fa pensare: "Potrei affrontare questi stili in un modo diverso?".

Esempi

Diamo un'occhiata ad alcuni esempi di come possiamo utilizzarlo.

Carte

Avvia una demo classica delle carte. Potremmo mostrare qualsiasi informazione nella nostra scheda, ad esempio un titolo, un sottotitolo o alcuni contenuti multimediali. Questa è la carta 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 si vogliono introdurre alcuni contenuti multimediali? Per questa struttura, la scheda potrebbe essere suddivisa in due colonne. Prima potresti creare una nuova classe per rappresentare questo comportamento, ad esempio card--with-media o card--two-columns. Questi nomi di classi non solo sono difficili da evocare, ma diventano anche difficili da mantenere e ricordare.

Con :has(), puoi rilevare che la scheda contiene contenuti multimediali e intraprendere le azioni appropriate. Non sono necessari i nomi delle classi di modifica.

<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ì. Puoi usare la tua creatività. In che modo una scheda che mostra contenuti "in primo piano" potrebbe adattarsi a un layout? Questo CSS applica una scheda in primo piano a tutta la 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);
}

Cosa succede se una scheda in primo piano con un banner ondeggia 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 lo stile difficile. Ne è un esempio l'applicazione di stili agli input e alle relative etichette. Ad esempio, come viene indicato che un campo è valido? Con :has(), è molto più facile. Possiamo agganciare le pseudo-classi 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 in questo esempio: prova a inserire valori validi e non validi e a concentrarti e disattivare lo stato attivo.

Puoi anche utilizzare :has() per mostrare e nascondere il messaggio di errore relativo a un campo. Aggiungi un messaggio di errore al nostro 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;
}

Tuttavia, quando il campo diventa :invalid e non è attivo, puoi mostrare il messaggio senza bisogno di nomi di classe aggiuntivi.

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

Non c'è motivo per cui non potresti aggiungere un pizzico di bizzarro stile quando gli utenti interagiscono con il modulo. Considera questo esempio. Controlla quando inserisci un valore valido per la micro-interazione. Un valore :invalid causerà la vibrazione del gruppo di moduli. Tuttavia, solo se l'utente non ha preferenze di movimento.

Contenuti

Ne abbiamo parlato negli esempi di codice. Ma come potresti utilizzare :has() nel flusso dei documenti? Vengono suggerite idee su come potremmo definire uno stile tipografico in base ai contenuti multimediali, ad esempio.

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. Quando non hanno figcaption, galleggiano all'interno dei contenuti. Quando è presente un figcaption, questo occupa l'intera larghezza e ha un margine extra.

Reazione allo Stato

Che ne dici di rendere i tuoi stili reattivi ad alcuni stati nel nostro markup? Considera un esempio con la barra di navigazione scorrevole "classica". Se è presente un pulsante che attiva/disattiva l'apertura del menu di navigazione, potrebbe essere utilizzato l'attributo aria-expanded. Puoi usare JavaScript per aggiornare gli attributi appropriati. Quando aria-expanded è true, usa :has() per rilevarlo e aggiornare gli stili per la navigazione scorrevole. JavaScript fa la sua parte e il CSS può fare quello che vuole con queste informazioni. Non c'è bisogno di spostare il markup del markup o aggiungere nomi di classi extra 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ò :aiutare a evitare errori dell'utente?

Cos'hanno in comune tutti questi esempi? A parte il fatto che mostrano modi per utilizzare :has(), nessuno di loro ha richiesto di modificare i nomi delle classi. Ognuno di questi ha inserito nuovi contenuti e aggiornato un attributo. Questo è un grande vantaggio di :has(), in quanto può contribuire a ridurre l'errore dell'utente. Con :has() il CSS è in grado di assumersi la responsabilità di adeguare le modifiche nel DOM. Non devi destreggiarti tra i nomi delle classi in JavaScript, riducendo il rischio di errori dello sviluppatore. Ci siamo passati tutti quando facciamo un errore di battitura del nome di una classe e dobbiamo ricorrere a mantenimento in Object ricerche.

È un pensiero interessante e ci porta a un markup più chiaro e a meno codice? Meno JavaScript, dato che non stiamo apportando altre modifiche a JavaScript. Meno HTML poiché non hai più bisogno di corsi come card card--has-media e così via.

Pensare fuori dagli schemi

Come accennato in precedenza, :has() ti incoraggia a violare il modello mentale. Ti offre l'opportunità di provare cose diverse. Per provare a superare i limiti, puoi creare le meccaniche di gioco utilizzando solo il CSS. Ad esempio, puoi creare un meccanico basato 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;
}

E questo apre possibilità interessanti. Potresti utilizzarlo per attraversare una modulo con le trasformazioni. Tieni presente che questa demo viene visualizzata al meglio in una scheda separata del browser.

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

Anche se a breve non li metterai in produzione, mettono in evidenza i modi in cui puoi utilizzare la funzione primitiva. Ad esempio, è possibile concatenare un :has().

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

Prestazioni e limitazioni

Prima di finire, cosa non puoi fare con :has()? Esistono alcune limitazioni con :has(). Quelli principali sono dovuti agli hit del rendimento.

  • Non puoi :has() un :has(). Puoi però concatenare un :has(). css :has(.a:has(.b)) { … }
  • Nessun utilizzo di pseudo elementi all'interno di :has() css :has(::after) { … } :has(::first-letter) { … }
  • Limita l'utilizzo di :has() all'interno di pseudo 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 pseudo elemento css ::part(foo):has(:focus) { … }
  • L'utilizzo di :visited sarà sempre falso css :has(:visited) { … }

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

È tutto!

Preparati per :has(). Parlane con i tuoi amici e condividi questo post: il modo in cui ci affidiamo a CSS sarà una svolta assoluta.

Tutte le demo sono disponibili in questa colllezione CodePen.