All'interno del polyfill delle query del container

Gerald Monaco
Gerald Monaco

Le query contenitore sono una nuova funzionalità CSS che consente di scrivere logiche di stile che hanno come target le caratteristiche di un elemento principale (ad esempio, la larghezza o l'altezza) per definire gli elementi secondari. Di recente, è stato rilasciato un importante aggiornamento al polyfill, in coincidenza con il supporto dei browser.

In questo post, potrai dare uno sguardo al funzionamento del polyfill, alle sfide che supera e alle best practice per utilizzarlo per offrire un'esperienza utente ottimale ai tuoi visitatori.

dietro le quinte

Traspilazione

Quando l'analizzatore sintattico CSS in un browser rileva una regola at sconosciuta, come la nuova regola @container, la ignora semplicemente come se non fosse mai esistita. Di conseguenza, la prima e più importante operazione che il polyfill deve eseguire è il transpile di una query @container in un elemento che non verrà ignorato.

Il primo passaggio della traspilazione consiste nel convertire la regola @container di primo livello in una query @media. In questo modo, i contenuti rimangono raggruppati. ad esempio quando utilizzi le API CSSOM e quando visualizzi il codice sorgente CSS.

Prima
@container (width > 300px) {
  /* content */
}
Dopo
@media all {
  /* content */
}

Prima delle query contenitore, il CSS non aveva un modo per un autore di attivare o disattivare arbitrariamente gruppi di regole. Per eseguire il polyfill di questo comportamento, anche le regole all'interno di una query container devono essere trasformate. A ogni @container viene assegnato il proprio ID univoco (ad esempio, 123), che viene utilizzato per trasformare ogni selettore in modo che venga applicato solo quando l'elemento ha un attributo cq-XYZ che include questo ID. Questo attributo verrà impostato dal polyfill in fase di runtime.

Prima
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Dopo
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Nota l'utilizzo della pseudo-classe :where(...). Normalmente, l'inclusione di un selettore di attributi aggiuntivo aumenterebbe la specificità del selettore. Con la pseudo-classe, la condizione aggiuntiva può essere applicata mantenendo la specificità originale. Per capire perché questo aspetto è fondamentale, considera l'esempio seguente:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Dato questo CSS, un elemento con la classe .card dovrebbe sempre avere color: red, poiché la regola successiva sostituirebbe sempre la regola precedente con lo stesso selettore e la stessa specificità. La compilazione della prima regola e l'inclusione di un selettore di attributi aggiuntivo senza :where(...) aumentano quindi la specificità e causano l'applicazione errata di color: blue.

Tuttavia, la pseudo-classe :where(...) è abbastanza nuova. Per i browser che non lo supportano, il polyfill fornisce una soluzione alternativa semplice e sicura: puoi aumentare intenzionalmente la specificità delle regole aggiungendo manualmente un selettore :not(.container-query-polyfill) fittizio alle tue regole @container:

Prima
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
Dopo
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Ciò presenta una serie di vantaggi:

  • Il selettore nel CSS di origine è cambiato, quindi la differenza di specificità è esplicitamente visibile. Funge anche da documentazione in modo che tu sappia quali sono le conseguenze se non devi più supportare la soluzione alternativa o il polyfill.
  • La specificità delle regole sarà sempre la stessa, poiché il polyfill non la modifica.

Durante la traspilazione, il polyfill sostituirà questo dummy con il selettore di attributi con la stessa specificità. Per evitare sorprese, il polyfill utilizza entrambi i selettori: viene utilizzato il selettore di origine originale per determinare se l'elemento deve ricevere l'attributo polyfill e il selettore trapelato per gli stili.

Pseudoelementi

Una domanda che potresti chiederti è: se il polyfill imposta un attributo cq-XYZ su un elemento per includere l'ID contenitore univoco 123, come possono essere supportati gli pseudo-elementi, che non possono avere attributi impostati?

Gli pseudo-elementi sono sempre associati a un elemento reale nel DOM, chiamato elemento di origine. Durante la traspilazione, il selettore condizionale viene applicato a questo elemento reale:

Prima
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Dopo
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Anziché essere trasformato in #foo::before:where([cq-XYZ~="123"]) (che non sarebbe valido), il selettore condizionale viene spostato alla fine dell'elemento di origine, #foo.

Tuttavia, non è tutto ciò che serve. Un container non può modificare nulla di ciò che non è contenuto al suo interno (e un contenitore non può essere al suo interno), ma tieni presente che è esattamente ciò che accadrebbe se #foo fosse stesso l'elemento contenitore oggetto della query. L'attributo #foo[cq-XYZ] verrebbe modificato per errore e qualsiasi regola #foo verrebbe applicata per errore.

Per risolvere questo problema, il polyfill utilizza in realtà due attributi: uno che può essere applicato solo a un elemento da un elemento principale e uno a cui un elemento può applicare se stesso. Quest'ultimo attributo viene utilizzato per i selettori che scelgono come target pseudo-elementi.

Prima
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Dopo
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Poiché un contenitore non applicherà mai il primo attributo (cq-XYZ-A), il primo selettore troverà una corrispondenza solo se un contenitore principale diverso ha soddisfatto le condizioni del contenitore e lo ha applicato.

Unità relative del container

Le query contenitore includono anche alcune nuove unità che puoi utilizzare nel tuo CSS, ad esempio cqw e cqh per l'1% della larghezza e dell'altezza (rispettivamente) del contenitore principale appropriato più vicino. Per supportarle, l'unità viene trasformata in un'espressione calc(...) utilizzando le proprietà personalizzate CSS. Il polyfill imposterà i valori di queste proprietà tramite gli stili in linea sull'elemento contenitore.

Prima
.card {
  width: 10cqw;
  height: 10cqh;
}
Dopo
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Esistono anche unità logiche, come cqi e cqb per la dimensione in linea e la dimensione del blocco (rispettivamente). Questi sono un po' più complicati, perché gli assi in linea e di blocco sono determinati dal writing-mode dell'elemento che utilizza l'unità, non dall'elemento sottoposto a query. A questo scopo, il polyfill applica uno stile incorporato a qualsiasi elemento con writing-mode diverso da quello principale.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Ora le unità possono essere trasformate nella proprietà personalizzata CSS appropriata, proprio come prima.

Proprietà

Le query contenitore aggiungono anche alcune nuove proprietà CSS come container-type e container-name. Poiché non è possibile utilizzare API come getComputedStyle(...) con proprietà sconosciute o non valide, anche queste vengono trasformate in proprietà personalizzate CSS dopo essere state analizzate. Se non è possibile analizzare una proprietà (ad esempio perché contiene un valore non valido o sconosciuto), non viene gestita dal browser.

Prima
.card {
  container-name: card-container;
  container-type: inline-size;
}
Dopo
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Queste proprietà vengono trasformate ogni volta che vengono scoperte, consentendo al polyfill di interagire correttamente con altre funzionalità CSS come @supports. Questa funzionalità è la base delle best practice per l'utilizzo di polyfill, come illustrato di seguito.

Prima
@supports (container-type: inline-size) {
  /* ... */
}
Dopo
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Per impostazione predefinita, le proprietà personalizzate CSS vengono ereditate, il che significa, ad esempio, che qualsiasi elemento secondario di .card assumerà il valore --cq-XYZ-container-name e --cq-XYZ-container-type. Questo non è certamente il comportamento delle proprietà native. Per risolvere questo problema, il polyfill inserisce la seguente regola prima di qualsiasi stile utente, assicurando che ogni elemento riceva i valori iniziali, a meno che non venga intenzionalmente sostituito da un'altra regola.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Best practice

Anche se si prevede che la maggior parte dei visitatori utilizzerà browser con il supporto integrato delle query sul contenitore il prima possibile, è comunque importante offrire ai visitatori rimanenti un'esperienza positiva.

Durante il caricamento iniziale, devono essere molte le cose da fare prima che il polyfill possa strutturare la pagina:

  • Il polyfill deve essere caricato e inizializzato.
  • I fogli di stile devono essere analizzati e transcompilati. Poiché non esistono API per accedere all'origine non elaborata di un foglio di stile esterno, potrebbe essere necessario ricaricarlo in modo asincrono, anche se idealmente solo dalla cache del browser.

Se questi problemi non vengono affrontati con attenzione dal polyfill, potrebbe potenzialmente regredire i tuoi Core Web Vitals.

Per consentirti di offrire ai visitatori un'esperienza piacevole, il polyfill è stato progettato per dare la priorità a First Input Delay (FID) e Cumulative Layout Shift (CLS), potenzialmente a scapito della Largest Contentful Paint (LCP). Concretamente, il polyfill non garantisce che le query del container vengano valutate prima della prima visualizzazione. Ciò significa che, per una migliore esperienza utente, devi assicurarti che tutti i contenuti le cui dimensioni o posizione potrebbero essere influenzate dall'utilizzo delle query contenitore siano nascosti fino a quando il polyfill non è stato caricato e caricato il CSS. Un modo per farlo è utilizzare una regola @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

Ti consigliamo di abbinare questa impostazione a un'animazione di caricamento CSS pura, collocata in modo assoluto sopra i tuoi contenuti (nascosti), per informare il visitatore che sta succedendo qualcosa. Puoi trovare una demo completa di questo approccio qui.

Questo approccio è consigliato per diversi motivi:

  • Un caricatore CSS puro riduce al minimo il carico di lavoro per gli utenti con browser più recenti, fornendo al contempo un feedback leggero a quelli su browser meno recenti e reti più lente.
  • Combinando il posizionamento assoluto del caricatore con visibility: hidden, eviterai la variazione del layout.
  • Una volta caricato il polyfill, la condizione @supports non verrà più trasmessa e i tuoi contenuti verranno rivelati.
  • Nei browser con supporto integrato per le query container, la condizione non verrà mai inviata e, di conseguenza, la pagina verrà visualizzata sul first-paint come previsto.

Conclusione

Se vuoi utilizzare le query contenitore sui browser meno recenti, prova polyfill. In caso di problemi, non esitare a segnalare un problema.

Non vediamo l'ora di vedere e sperimentare le fantastiche cose che costruirai con questo strumento.

Ringraziamenti

Immagine hero di Dan Cristian Pădureț su Unsplash.