Containerquery's zijn een nieuwe CSS-functie waarmee u stijllogica kunt schrijven die zich richt op kenmerken van een bovenliggend element (bijvoorbeeld de breedte of hoogte) om de onderliggende elementen op te maken. Onlangs is er een grote update voor de polyfill uitgebracht, die samenviel met de ondersteuning in browsers.
In dit bericht kun je een kijkje nemen in de manier waarop polyfill werkt, welke uitdagingen het overwint en wat de best practices zijn bij het gebruik ervan om je bezoekers een geweldige gebruikerservaring te bieden.
Onder de motorkap
Transpilatie
Wanneer de CSS-parser in een browser een onbekende at-regel tegenkomt, zoals de gloednieuwe @container
-regel, zal deze deze weggooien alsof deze nooit heeft bestaan. Daarom is het eerste en belangrijkste wat de polyfill moet doen een @container
-query omzetten in iets dat niet zal worden weggegooid.
De eerste stap bij transpilatie is het converteren van de @container
regel op het hoogste niveau naar een @media- query. Dit zorgt er meestal voor dat de inhoud gegroepeerd blijft. Bijvoorbeeld bij het gebruik van CSSOM API’s en bij het bekijken van de CSS-bron.
@container (width > 300px) { /* content */ }
@media all { /* content */ }
Vóór containerquery's had CSS geen manier voor een auteur om willekeurig groepen regels in of uit te schakelen. Om dit gedrag te polyfillen, moeten de regels binnen een containerquery ook worden getransformeerd. Elke @container
krijgt zijn eigen unieke ID (bijvoorbeeld 123
), die wordt gebruikt om elke selector zo te transformeren dat deze alleen van toepassing is als het element een cq-XYZ
-attribuut heeft dat deze ID bevat. Dit kenmerk wordt tijdens runtime door de polyfill ingesteld.
@container (width > 300px) { .card { /* ... */ } }
@media all { .card:where([cq-XYZ~="123"]) { /* ... */ } }
Let op het gebruik van de :where(...)
pseudo-klasse. Normaal gesproken zou het opnemen van een extra attribuutselector de specificiteit van de selector vergroten. Met de pseudo-klasse kan de extra voorwaarde worden toegepast met behoud van de oorspronkelijke specificiteit. Bekijk het volgende voorbeeld om te zien waarom dit cruciaal is:
@container (width > 300px) {
.card {
color: blue;
}
}
.card {
color: red;
}
Gegeven deze CSS zou een element met de klasse .card
altijd color: red
moeten hebben, omdat de latere regel altijd de vorige regel zou overschrijven met dezelfde selector en specificiteit. Het transpileren van de eerste regel en het toevoegen van een extra attribuutselector zonder :where(...)
zou daarom de specificiteit vergroten en ervoor zorgen dat color: blue
ten onrechte wordt toegepast.
De :where(...)
pseudo-klasse is echter vrij nieuw . Voor browsers die dit niet ondersteunen, biedt de polyfill een veilige en gemakkelijke oplossing: u kunt opzettelijk de specificiteit van uw regels vergroten door handmatig een dummy :not(.container-query-polyfill)
selector toe te voegen aan uw @container
-regels:
@container (width > 300px) { .card { color: blue; } } .card { color: red; }
@container (width > 300px) { .card:not(.container-query-polyfill) { color: blue; } } .card { color: red; }
Dit heeft een aantal voordelen:
- De selector in de bron-CSS is gewijzigd, waardoor het verschil in specificiteit expliciet zichtbaar is. Dit fungeert ook als documentatie, zodat u weet wat er van invloed is als u de tijdelijke oplossing of de polyfill niet langer hoeft te ondersteunen.
- De specificiteit van de regels zal altijd hetzelfde zijn, omdat de polyfill deze niet verandert.
Tijdens transpilatie zal de polyfill deze dummy vervangen door de attribuutselector met dezelfde specificiteit. Om verrassingen te voorkomen, gebruikt de polyfill beide selectors: de oorspronkelijke bronselector wordt gebruikt om te bepalen of het element het polyfill-attribuut moet krijgen, en de getranspileerde selector wordt gebruikt voor de stijl.
Pseudo-elementen
Eén vraag die u zich misschien stelt is: als de polyfill een cq-XYZ
attribuut op een element instelt om de unieke container-ID 123
op te nemen, hoe kunnen dan pseudo-elementen, waarvoor geen attributen kunnen worden ingesteld, worden ondersteund?
Pseudo-elementen zijn altijd gebonden aan een reëel element in de DOM, het oorspronkelijke element genoemd. Tijdens transpilatie wordt in plaats daarvan de voorwaardelijke selector op dit reële element toegepast:
@container (width > 300px) { #foo::before { /* ... */ } }
@media all { #foo:where([cq-XYZ~="123"])::before { /* ... */ } }
In plaats van te worden getransformeerd naar #foo::before:where([cq-XYZ~="123"])
(wat ongeldig zou zijn), wordt de voorwaardelijke selector verplaatst naar het einde van het oorspronkelijke element, #foo
.
Dat is echter niet alles wat nodig is. Een container mag niets wijzigen dat er niet in zit (en een container kan zich ook niet in zichzelf bevinden), maar bedenk dat dit precies is wat er zou gebeuren als #foo
zelf het containerelement zou zijn dat wordt opgevraagd. Het #foo[cq-XYZ]
attribuut zou ten onrechte worden gewijzigd en eventuele #foo
regels zouden ten onrechte worden toegepast.
Om dit te corrigeren gebruikt de polyfill eigenlijk twee attributen: één die alleen door een ouder op een element kan worden toegepast, en één die een element op zichzelf kan toepassen. Dit laatste attribuut wordt gebruikt voor selectors die zich op pseudo-elementen richten.
@container (width > 300px) { #foo, #foo::before { /* ... */ } }
@media all { #foo:where([cq-XYZ-A~="123"]), #foo:where([cq-XYZ-B~="123"])::before { /* ... */ } }
Omdat een container nooit het eerste attribuut ( cq-XYZ-A
) op zichzelf zal toepassen, komt de eerste selector alleen overeen als een andere bovenliggende container aan de containervoorwaarden heeft voldaan en deze heeft toegepast.
Relatieve eenheden van containers
Containerquery's worden ook geleverd met een paar nieuwe eenheden die u in uw CSS kunt gebruiken, zoals cqw
en cqh
voor 1% van de breedte en hoogte (respectievelijk) van de dichtstbijzijnde geschikte bovenliggende container. Ter ondersteuning hiervan wordt de eenheid omgezet in een calc(...)
-expressie met behulp van CSS Custom Properties . De polyfill stelt de waarden voor deze eigenschappen in via inlinestijlen op het containerelement.
.card { width: 10cqw; height: 10cqh; }
.card { width: calc(10 * --cq-XYZ-cqw); height: calc(10 * --cq-XYZ-cqh); }
Er zijn ook logische eenheden, zoals cqi
en cqb
voor respectievelijk inlinegrootte en blokgrootte. Deze zijn iets ingewikkelder, omdat de inline- en blokassen worden bepaald door de writing-mode
van het element dat de eenheid gebruikt , en niet door het element dat wordt opgevraagd. Om dit te ondersteunen past de polyfill een inline-stijl toe op elk element waarvan writing-mode
verschilt van het bovenliggende element.
/* 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);
Nu kunnen de eenheden net als voorheen worden omgezet in de juiste aangepaste CSS-eigenschap.
Eigenschappen
Containerquery's voegen ook een paar nieuwe CSS-eigenschappen toe, zoals container-type
en container-name
. Omdat API's zoals getComputedStyle(...)
niet kunnen worden gebruikt met onbekende of ongeldige eigenschappen, worden deze ook getransformeerd naar aangepaste CSS-eigenschappen nadat ze zijn geparseerd . Als een eigenschap niet kan worden geparseerd (bijvoorbeeld omdat deze een ongeldige of onbekende waarde bevat), wordt deze gewoon aan de browser overgelaten.
.card { container-name: card-container; container-type: inline-size; }
.card { --cq-XYZ-container-name: card-container; --cq-XYZ-container-type: inline-size; }
Deze eigenschappen worden getransformeerd wanneer ze worden ontdekt, waardoor de polyfill goed kan worden gecombineerd met andere CSS-functies zoals @supports
. Deze functionaliteit vormt de basis van de best practices voor het gebruik van de polyfill, zoals hieronder beschreven.
@supports (container-type: inline-size) { /* ... */ }
@supports (--cq-XYZ-container-type: inline-size) { /* ... */ }
Standaard worden aangepaste CSS-eigenschappen overgenomen, wat bijvoorbeeld betekent dat elk kind van .card
de waarde van --cq-XYZ-container-name
en --cq-XYZ-container-type
zal aannemen. Dat is absoluut niet hoe de inheemse eigenschappen zich gedragen. Om dit op te lossen, zal de polyfill de volgende regel vóór eventuele gebruikersstijlen invoegen, zodat elk element de initiële waarden ontvangt, tenzij het opzettelijk wordt overschreven door een andere regel.
* {
--cq-XYZ-container-name: none;
--cq-XYZ-container-type: normal;
}
Beste praktijken
Hoewel verwacht wordt dat de meeste bezoekers vroeg of laat browsers zullen gebruiken met ingebouwde ondersteuning voor containerquery's, is het nog steeds belangrijk om de overige bezoekers een goede ervaring te bieden.
Tijdens het eerste laden moet er veel gebeuren voordat de polyfill de pagina kan opmaken:
- De polyfill moet worden geladen en geïnitialiseerd.
- Stylesheets moeten worden geparseerd en getranspileerd. Omdat er geen API's zijn om toegang te krijgen tot de onbewerkte bron van een extern stylesheet, moet het mogelijk asynchroon opnieuw worden opgehaald, maar idealiter alleen uit de browsercache.
Als deze problemen niet zorgvuldig worden aangepakt door de polyfill, kan dit mogelijk uw Core Web Vitals verminderen.
Om het voor u gemakkelijker te maken uw bezoekers een prettige ervaring te bieden, is de polyfill ontworpen om prioriteit te geven aan First Input Delay (FID) en Cumulative Layout Shift (CLS) , mogelijk ten koste van Largest Contentful Paint (LCP) . Concreet biedt de polyfill geen garantie dat uw containervragen vóór de eerste verf worden beoordeeld . Dit betekent dat u voor de beste gebruikerservaring ervoor moet zorgen dat alle inhoud waarvan de grootte of positie zou worden beïnvloed door het gebruik van containerquery's verborgen blijft totdat de polyfill uw CSS heeft geladen en getranspileerd. Eén manier om dit te bereiken is door een @supports
-regel te gebruiken:
@supports not (container-type: inline-size) {
#content {
visibility: hidden;
}
}
Het is aan te raden dit te combineren met een pure CSS-laadanimatie, absoluut over uw (verborgen) inhoud gepositioneerd, om de bezoeker te vertellen dat er iets gebeurt. Een volledige demo van deze aanpak vindt u hier .
Deze aanpak wordt om een aantal redenen aanbevolen:
- Een pure CSS-lader minimaliseert de overhead voor gebruikers met nieuwere browsers, terwijl het lichtgewicht feedback biedt aan gebruikers van oudere browsers en langzamere netwerken.
- Door absolute positionering van de lader te combineren met
visibility: hidden
, vermijdt u indelingsverschuiving. - Nadat de polyfill is geladen, stopt deze
@supports
voorwaarde en wordt uw inhoud onthuld. - Op browsers met ingebouwde ondersteuning voor containerquery's zal de voorwaarde nooit passeren, en dus wordt de pagina zoals verwacht weergegeven in de eerste verf.
Conclusie
Als u geïnteresseerd bent in het gebruik van containerquery's in oudere browsers, probeer dan de polyfill eens. Aarzel niet om een probleem aan te geven als u problemen ondervindt.
We kunnen niet wachten om de geweldige dingen die je ermee gaat bouwen te zien en ervaren.