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 abbia come target le funzionalità di un elemento principale (ad esempio la larghezza o l'altezza) per applicare uno stile ai relativi elementi secondari. Di recente è stato rilasciato un grande aggiornamento del polyfill, in coincidenza con l'introduzione del supporto nei browser.

In questo post, potrai dare un'occhiata al funzionamento del polyfill, alle sfide che supera e alle best practice per utilizzarlo al meglio al fine di offrire un'esperienza utente eccezionale ai tuoi visitatori.

dietro le quinte

Transpilazione

Quando l'analizzatore sintattico CSS all'interno di un browser rileva una regola at sconosciuta, come la nuova regola @container, la ignora come se non fosse mai esistita. Pertanto, la prima e più importante cosa che il polyfill deve fare è transpilare una query @container in qualcosa che non verrà eliminato.

Il primo passaggio della transpilazione 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 del contenitore, CSS non offriva un modo per consentire o disattivare arbitrariamente gruppi di regole. Per eseguire il polyfill di questo comportamento, è necessario trasformare anche le regole all'interno di una query del contenitore. A ogni @container viene assegnato un ID univoco (ad es. 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 esecuzione.

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

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

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

.card {
  color: red;
}

In questo CSS, un elemento con la classe .card deve sempre avere color: red, poiché la regola successiva sostituirà sempre la regola precedente con lo stesso selettore e la stessa specificità. La transpilazione della prima regola e l'inclusione di un selettore di attributi aggiuntivo senza :where(...) aumenterebbe quindi la specificità e causerebbe l'applicazione errata di color: blue.

Tuttavia, l'pseudo-classe :where(...) è abbastanza recente. Per i browser che non lo supportano, il polyfill fornisce una soluzione alternativa sicura e semplice: puoi intenzionalmente aumentare 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 approccio presenta diversi vantaggi:

  • Il selettore nel CSS di origine è cambiato, quindi la differenza di specificità è visibile in modo esplicito. Inoltre, funge da documentazione per sapere cosa è interessato quando non è più necessario supportare la soluzione alternativa o il polyfill.
  • La specificità delle regole sarà sempre la stessa, poiché il polyfill non la modifica.

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

Pseudo-elementi

Una domanda che potresti porti è: 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 transpilazione, il selettore condizionale viene applicato a questo elemento reale:

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

Invece di 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 contenitore non può modificare nulla che non sia contenuto al suo interno (e un contenitore non può essere all'interno di se stesso), ma tieni presente che è esattamente ciò che accadrebbe se #foo fosse l'elemento contenitore su cui viene eseguita la query. L'attributo #foo[cq-XYZ] verrebbe modificato erroneamente e eventuali regole #foo verrebbero applicate erroneamente.

Per correggere questo problema, il polyfill utilizza in realtà due attributi: uno che può essere applicato a un elemento solo da un elemento principale e uno che un elemento può applicare a se stesso. Quest'ultimo attributo viene utilizzato per i selettori che hanno come target gli 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) a se stesso, il primo selettore corrisponderà solo se un contenitore principale diverso ha soddisfatto le condizioni del contenitore e le ha applicate.

Unità relative al contenitore

Le query del contenitore includono anche alcune nuove unità che puoi utilizzare nel 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à CSS personalizzate. Il polyfill imposterà i valori per queste proprietà tramite gli stili in linea 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 le dimensioni in linea e dei blocchi (rispettivamente). Questi sono un po' più complicati, perché gli assi in linea e a blocchi sono determinati dall'attributo writing-mode dell'elemento che utilizza l'unità, non dell'elemento su cui viene eseguita la query. Per supportare questa funzionalità, il polyfill applica uno stile in linea a qualsiasi elemento il cui writing-mode è diverso da quello del relativo 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à CSS personalizzata appropriata, come prima.

Proprietà

Le query dei contenitori 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à CSS personalizzate dopo l'analisi. Se una proprietà non può essere analizzata (ad esempio perché contiene un valore non valido o sconosciuto), viene semplicemente lasciata al browser per la gestione.

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 rilevate, consentendo al polyfill di interagire correttamente con altre funzionalità CSS come @supports. Questa funzionalità è alla base delle best practice per l'utilizzo del polyfill, come descritto di seguito.

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

Per impostazione predefinita, le proprietà CSS personalizzate 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 è assolutamente il comportamento delle proprietà native. Per risolvere il problema, il polyfill inserirà la seguente regola prima di qualsiasi stile utente, assicurandosi che ogni elemento riceva i valori iniziali, a meno che non vengano intenzionalmente sostituiti da un'altra regola.

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

Best practice

Sebbene sia previsto che la maggior parte dei visitatori utilizzi browser con il supporto integrato delle query dei contenitori prima o poi, è comunque importante offrire un'esperienza positiva ai visitatori rimanenti.

Durante il caricamento iniziale, devono verificarsi molti eventi prima che il polyfill possa eseguire il layout della pagina:

  • Il polyfill deve essere caricato e inizializzato.
  • Gli stili devono essere analizzati e transpilati. Poiché non esistono API per accedere al codice sorgente non elaborato di un foglio di stile esterno, potrebbe essere necessario recuperarlo nuovamente in modo asincrono, anche se idealmente solo dalla cache del browser.

Se questi problemi non vengono affrontati con attenzione dal polyfill, potrebbero verificarsi dei cali dei Segnali web essenziali.

Per semplificare la creazione di un'esperienza piacevole per i visitatori, il polyfill è stato progettato per dare la priorità a First Input Delay (FID) e Cumulative Layout Shift (CLS), potenzialmente a scapito di Largest Contentful Paint (LCP). Nello specifico, il polyfill non garantisce che le query del contenitore verranno valutate prima della prima visualizzazione. Ciò significa che, per un'esperienza utente ottimale, devi assicurarti che tutti i contenuti le cui dimensioni o la cui posizione verrebbero interessate dall'utilizzo delle query del contenitore siano nascosti fino a quando il polyfill non avrà caricato e transpilato il CSS. Un modo per farlo è utilizzare una regola @supports:

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

Ti consigliamo di combinare questa opzione con un'animazione di caricamento in puro CSS, posizionata in modo assoluto sopra i contenuti (nascosti), per comunicare al 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 sovraccarico per gli utenti con browser più recenti, fornendo al contempo un feedback leggero a chi utilizza browser meno recenti e reti più lente.
  • Se combini il posizionamento assoluto del caricatore con visibility: hidden, eviti lo spostamento del layout.
  • Una volta caricato il polyfill, questa condizione @supports non verrà più passata e i tuoi contenuti verranno visualizzati.
  • Nei browser con il supporto integrato per le query dei contenitori, la condizione non verrà mai soddisfatta, pertanto la pagina verrà visualizzata al primo caricamento come previsto.

Conclusione

Se ti interessa utilizzare le query dei contenitori su browser meno recenti, prova il polyfill. Non esitare a segnalare un problema se riscontri difficoltà.

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