All'interno del polyfill delle query del container

Gerald Monaco
Gerald Monaco

Le query contenitore sono una nuova funzionalità CSS che ti consente di scrivere una logica di stile che ha come target le caratteristiche di un elemento principale (ad esempio, la sua larghezza o altezza) per definire gli stili secondari. Di recente è stato rilasciato un importante aggiornamento al polyfill, in concomitanza con l'arrivo del supporto nei browser.

In questo post, potrai dare un'occhiata al funzionamento del polyfill, alle sfide che può superare e alle best practice da utilizzare per offrire ai visitatori un'esperienza ottimale.

Uno sguardo alle caratteristiche

Traspilazione

Quando il parser CSS all'interno di un browser rileva una regola "at" sconosciuta, come la nuova regola @container, la elimina come se non fosse mai esistita. Di conseguenza, la prima e più importante operazione che il polyfill deve eseguire è trasporre una query @container in un elemento che non verrà eliminato.

Il primo passaggio nel processo di traspilazione è convertire la regola @container di primo livello in una query @media. Ciò garantisce principalmente che i contenuti rimangano raggruppati. Ad esempio, quando utilizzi le API CSSOM e quando visualizzi l'origine CSS.

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

Prima delle query relative al container, il CSS non aveva un modo per consentire a un autore di attivare o disattivare arbitrariamente i gruppi di regole. Per eseguire il polyfill di questo comportamento, è necessario trasformare anche le regole all'interno di una query container. 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 al momento dell'attivazione.

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

Osserva l'utilizzo della pseudo-classe :where(...). Normalmente, l'inclusione di un ulteriore selettore di attributi aumenta la specificità del selettore. Con la pseudo-classe, è possibile applicare la condizione aggiuntiva mantenendo la specificità originale. Per capire perché questo aspetto è fondamentale, considera il seguente esempio:

@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 andrebbe sempre a sostituire la regola precedente con lo stesso selettore e la stessa specificità. Di conseguenza, il transcompilazione della prima regola e l'inclusione di un selettore di attributi aggiuntivo senza :where(...) aumenterebbe la specificità e causerebbe l'applicazione errata di color: blue.

Tuttavia, la pseudo-classe :where(...) è abbastanza nuova. Per i browser che non lo supportano, il polyfill offre una soluzione alternativa semplice e sicura: puoi aumentare intenzionalmente la specificità delle regole aggiungendo manualmente un selettore :not(.container-query-polyfill) fittizio alle 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;
}

Questo offre una serie di vantaggi:

  • Il selettore nel CSS di origine è cambiato, pertanto la differenza di specificità è chiaramente visibile. Queste informazioni fungono anche da documentazione per consentirti di sapere quali elementi sono interessati quando non hai più bisogno di supportare la soluzione alternativa o il polyfill.
  • La specificità delle regole rimarrà sempre la stessa, poiché il polyfill non lo modifica.

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

Pseudo-elementi

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, per i quali non è possibile impostare attributi?

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 tutto ciò che non è contenuto al suo interno (e un container non può essere al suo interno), ma considera esattamente ciò che accadrebbe se #foo fosse stesso l'elemento container su cui viene eseguita la query. L'attributo #foo[cq-XYZ] verrebbe modificato per errore e le eventuali regole #foo verrebbero applicate 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 l'altro che può essere applicato a se stesso. Il secondo attributo viene utilizzato per i selettori che hanno 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 corrisponderà solo se un contenitore principale diverso ha soddisfatto le condizioni del contenitore e lo ha applicato.

Unità relative al contenitore

Le query relative al 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 più vicino. A questo scopo, l'unità viene trasformata in un'espressione calc(...) utilizzando le proprietà personalizzate CSS. Il polyfill imposterà i valori per queste proprietà tramite stili incorporati nell'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 incorporata e la dimensione del blocco (rispettivamente). Queste sono un po' più complicate, perché gli assi incorporati e a blocchi sono determinati dall'elemento 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 il cui writing-mode è diverso dall'elemento 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 come in precedenza.

Proprietà

Le query contenitore aggiungono anche alcune nuove proprietà CSS, come container-type e container-name. Poiché API come getComputedStyle(...) non possono essere utilizzate 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), viene semplicemente lasciata in mano al 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 funzionare perfettamente con altre funzionalità CSS come @supports. Questa funzionalità è alla base delle best practice per l'utilizzo del polyfill, descritte 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 di --cq-XYZ-container-name e --cq-XYZ-container-type. Questo non è certo il comportamento delle proprietà native. Per risolvere questo problema, il polyfill inserisce la seguente regola prima di ogni stile utente, garantendo 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

Nonostante si preveda che la maggior parte dei visitatori utilizzi al più presto i browser con supporto integrato per le query dei container, è comunque importante offrire ai visitatori rimanenti una buona esperienza.

Prima che il polyfill possa definire il layout della pagina durante il caricamento iniziale, sono necessari molti interventi:

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

Se il polyfill non risolve questi problemi con attenzione, potrebbe far retrocedere i tuoi Segnali web essenziali.

Per consentirti di offrire più facilmente 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 un'esperienza utente ottimale, devi assicurarti che tutti i contenuti le cui dimensioni o posizione potrebbero essere interessate dall'utilizzo delle query del container siano nascosti fino al termine del caricamento e del transpile del tuo CSS da parte del polyfill. Un modo per farlo è utilizzare una regola @supports:

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

Ti consigliamo di combinare questa funzione con un'animazione di caricamento CSS pura, posizionata assolutamente sopra i tuoi contenuti (nascosti), per comunicare al visitatore che sta succedendo qualcosa. Puoi trovare una demo completa di questo approccio qui.

Questo approccio è consigliato per una serie di motivi:

  • Un caricatore CSS puro riduce al minimo l'overhead per gli utenti con browser più recenti, fornendo al contempo un feedback leggero a quelli che utilizzano browser meno recenti e reti più lente.
  • Combinando il posizionamento assoluto del caricatore con visibility: hidden, eviti una variazione del layout.
  • Una volta caricato il polyfill, questa condizione @supports smetterà di trasmettere e i tuoi contenuti verranno rivelati.
  • Nei browser con supporto integrato per le query relative ai container, la condizione non viene mai trasmessa, quindi la pagina verrà visualizzata come previsto nella prima visualizzazione.

Conclusione

Se vuoi utilizzare query di container nei browser meno recenti, prova il polyfill. Non esitare a segnalare un problema in caso di problemi.

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

Ringraziamenti

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