Container Queries: en komponent, der kender sin container, ikke viewportet
Container Queries: en komponent, der kender sin container, ikke viewportet
Du har et produktkort. Billede over titlen, pris, en knap med teksten "Læg i kurv". I butikkens hoved-grid, ved skærmbredder over 768px, skifter en media query til et vandret layout – billede til venstre, indhold til højre, fordi det udnytter den tilgængelige bredde bedre. Det virker fint. Så vil produktafdelingen have det samme kort i en smal sidebar med "anbefalede produkter" ved siden af artiklens hovedindhold. Du smider komponenten ind uden ændringer – og den går i stykker. Det vandrette layout, designet til 700+ pixels, prøver at presse sig ind i en 260-pixel bred kolonne. Billedet klemmes til en smal stribe, teksten ombrydes til ét ord pr. linje.
Du har ikke lavet en fejl i CSS'en. Din media query gør præcis det, den skal – den svarer bare på det forkerte spørgsmål. @media (min-width: 768px) spørger: "hvor bred er viewporten?". For at se godt ud burde produktkortet aldrig have skullet bekymre sig om viewportens bredde – det burde bekymre sig om, hvor meget plads det har fået af sin forælder. Det er to forskellige spørgsmål, som CSS i to årtier har behandlet som ét, fordi der ikke fandtes en måde at stille det andet på.
Hvorfor en media query ikke er nok
En media query er et globalt spørgsmål. Det er ligegyldigt, om din komponent sidder i en kolonne på 1200 pixels eller i et smalt sidekort på 260 – @media (min-width: 768px) returnerer true i begge tilfælde, fordi den spørger til browservinduet, ikke til elementets forælder. En komponent bygget udelukkende på media queries er derfor kun korrekt i den kontekst, den oprindeligt blev designet til – og holder op med at være korrekt, så snart den samme kode havner et andet sted. Det underminerer grundlæggende komponenters løfte: at de trygt kan genbruges hvor som helst i layoutet.
Før 2023 var den eneste reelle løsning ResizeObserver i JavaScript – du observerer et element, måler dets bredde ved hver ændring, gemmer resultatet i state og tilføjer betinget en CSS-klasse. Det virker, men koster: ekstra komponent-state, gen-rendering ved hver størrelsesændring, kode du skal skrive manuelt for hver enkelt komponent, og som ikke findes, før JavaScript er kørt – så ved server-side rendering vil det første billede alligevel vise layoutet "blindt", inden ResizeObserver når at måle noget som helst.
jsx
// Løsningen fra før Container Queries – virker, men kosterimport { useEffect, useRef, useState } from "react";const ProductCard = ({ product }) => {const cardRef = useRef(null);const [isWide, setIsWide] = useState(false);useEffect(() => {const el = cardRef.current;const observer = new ResizeObserver((entries) => {setIsWide(entries[0].contentRect.width > 320);});observer.observe(el);return () => observer.disconnect();}, []);return (<article ref={cardRef} className={`card ${isWide ? "card--wide" : ""}`}>{/* ... */}</article>);};
Container Queries løser det samme problem uden en eneste linje JavaScript, uden state og uden gen-rendering – for spørgsmålet "hvor meget plads har jeg til rådighed" havner igen dér, hvor CSS altid har stillet det: i stylesheetet.
Sådan virker det egentlig under motorhjelmen: containment, ikke bare ny syntaks
Selve @container-syntaksen ligner @media med et andet navn – men forskellen ligger et andet sted, i det, der skal ske, før browseren overhovedet accepterer at besvare et sådant spørgsmål. For at et element kan være en "query-container", skal du eksplicit erklære det:
css
.card-slot {container-type: inline-size;container-name: card;}@container card (min-width: 400px) {.card {grid-template-columns: 40% 1fr;}}
container-type: inline-size er ikke bare en kontakt, der slår query-tilstand til – det er en erklæring om CSS Containment, en separat specifikation, som fortæller browseren: "indholdet af dette element påvirker ikke dets størrelse i inline-aksen, så du kan trygt behandle det som en uafhængig ø under layout-beregningen". Det er ikke en ligegyldig implementeringsdetalje – det er en nødvendig betingelse for, at container queries overhovedet kan eksistere uden at gå i selvsving. Hvis browseren tillod et element samtidig at ændre størrelse som reaktion på sin egen bredde og påvirke den bredde gennem sit indhold, ville der opstå en cirkulær afhængighed: indholdet ændrer containerens størrelse → størrelsesændringen slår @container til → nye regler ændrer indholdet → indholdet ændrer containerens størrelse igen. Containment skærer den løkke over ved roden ved at tvinge containerens størrelse i den givne akse til at blive fastsat uafhængigt af, hvad der er indeni.
Det har en konkret, praktisk konsekvens for valget af container-type:
inline-size– containment kun i inline-aksen (typisk den vandrette). Elementets højde følger stadig frit af indholdet. Det er den værdi, du griber til i 95% af tilfældene – præcis som i eksemplet med produktkortet.size– containment i begge akser på én gang. Elementet holder helt op med at basere sin størrelse på sit indhold – hvilket betyder, at du skal give det en eksplicit højde (fx viaheightelleraspect-ratio), ellers skærer containment det fra dets naturlige indholdshøjde, og højden falder reelt til nul.normal– standardværdien, ingen containment, elementet er ikke en query-container.
container-name er valgfri, men værd at gøre til en vane – uden den vil @container (min-width: 400px) spørge den nærmeste forfader, der er en container, uanset hvilken det er. I et fladt komponent er det uskadeligt, men i et indlejret layout (et kort inde i et panel, panelet inde i en kolonne – og både panel og kolonne er containere) fortæller et navngivet container-navn entydigt, hvilken forfader du konkret spørger til, i stedet for at stole på tilfældig nærhed i DOM-træet.
Praktisk eksempel: ét kort, to kontekster, nul JavaScript
Tilbage til produktkortet fra indledningen – denne gang bygget sådan, at den samme komponent gengives korrekt både i det brede grid og i den smalle sidebar, uden nogen viden om, hvor den bliver placeret.
jsx
const ProductCard = ({ product }) => (<div className="product-card-slot"><article className="product-card"><imgsrc={product.image}alt={product.name}className="product-card__image"/><div className="product-card__body"><h3 className="product-card__title">{product.name}</h3><p className="product-card__price">{product.price} kr</p><button type="button" className="product-card__cta">Læg i kurv</button></div></article></div>);export default ProductCard;
css
/* Komponentens forælder erklærer sig selv som query-container */.product-card-slot {container-type: inline-size;container-name: product-card;}/* Standardlayout: billede over indhold – sikkert i smal plads */.product-card {display: grid;grid-template-columns: 1fr;gap: 12px;}.product-card__image {width: 100%;aspect-ratio: 4 / 3;object-fit: cover;border-radius: 8px;}/* Over 360px bredde på CONTAINEREN, ikke viewporten – skift til vandret layout */@container product-card (min-width: 360px) {.product-card {grid-template-columns: 40% 1fr;align-items: center;}.product-card__image {aspect-ratio: 1 / 1;height: 100%;}}
Den afgørende beslutning er gemt i strukturen, ikke i CSS'en: container-type sidder på .product-card-slot, altså kortets forælder, ikke på selve .product-card. Det er ikke tilfældigt eller overdrevet forsigtigt – specifikationen udelukker udtrykkeligt, at et element kan spørge til sin egen størrelse via @container (igen det samme problem med cirkulær afhængighed: et element kan ikke både definere en container og blive stylet som reaktion på dens størrelse). Derfor er det gennemgående, genkendelige mønster en tynd wrapper-container udenfor og selve komponenten indeni, stylet inden i en @container-blok. Den samme CSS-fil, brugt én gang i produktgrid'et (hvor slotten er 480px) og én gang i sidebaren (hvor slotten er 240px), giver to forskellige, korrekte layouts – uden en prop, uden en modifikator-klasse, uden en eneste linje JavaScript.
Container-enheder: flydende design uden viewport
Container Queries bragte en anden, mindre kendt del af den samme specifikation med sig: enheder relative til containeren, ikke viewporten – cqw (1% af containerens bredde), cqh (1% af højden), cqi og cqb (1% i inline/block-aksen, altså også korrekt i vertikalt skrevne sprog), samt cqmin/cqmax, analoge til vmin/vmax, men beregnet ud fra containerens mindste eller største dimension.
Deres naturlige anvendelse er flydende typografi inde i en komponent, uafhængig af, hvor bred brugerens skærm end er:
css
.product-card__title {/* Skalerer med kortets bredde, ikke skærmens bredde */font-size: clamp(1rem, 0.85rem + 2cqi, 1.375rem);}
Forskellen fra det velkendte clamp()-trick med vw-enheden er subtil, men i praksis fundamental: en titel skaleret med vw ændrer størrelse sammen med hele siden, så to kopier af det samme kort – ét i det fuldbrede grid, ét i den smalle sidebar – får identisk skriftstørrelse, fordi de begge ser den samme viewport. En titel skaleret med cqi reagerer på den faktiske plads, det pågældende kort har fået – i sidebaren bliver den mindre, i grid'et større, selvom viewporten hele tiden har præcis samme bredde.
Faldgruber og god praksis
container-type: sizeuden eksplicit højde æder indholdet. Containment i block-aksen skærer elementet fra sine efterkommeres naturlige højde – hvis du ikke angiverheightelleraspect-ratio, krymper containeren reelt til nul højde, og dens børn forsvinder visuelt, selvom de stadig findes i DOM'en. I langt de fleste tilfælde erinline-sizenok, og det problem opstår slet ikke.- En container kan ikke spørge til sig selv. Hvis du kæmper med, at
@container"ikke virker", er den første mistanke netop dette:container-typeog selektoren, der stylet af@container, sidder på det samme element. Adskil dem –container-typepå wrapperen, målstilarterne på barnet, præcis som i eksemplet med produktkortet ovenfor. - Container queries erstatter ikke media queries – de supplerer dem. Media query er stadig det rette værktøj til beslutninger på side-niveau (fx om en sidebar overhovedet skal vises, eller om navigationen skal skifte til en hamburgermenu). Container query håndterer beslutninger på komponent-niveau, uanset hvor den komponent ender med at lande. Et godt layout i 2026 bruger typisk begge dele, hver især dér, hvor det spørgsmål, de stiller, faktisk passer.
- Gør ikke hver eneste div til en container "for en sikkerheds skyld". Containment har en reel omkostning for rendering-motoren – det er et signal om "behandl mig som en isoleret layout-enhed", nyttigt dér, hvor du faktisk har en komponent, der reagerer på sin egen størrelse, ikke en standardindstilling for hele DOM-træet.
- Browserunderstøttelse er ikke længere et problem.
container-typeog@containeropnåede Baseline-status (bredt tilgængelig) i begyndelsen af 2023 – de virker nativt i Chrome, Safari og Firefox uden polyfills og uden@supports. Enhedernecqw/cqiog resten af familien har præcis samme dækning.
Opsummering
En genbrugelig komponent er ikke én, der har nok props og modifikator-klasser til manuelt at håndtere hvert eneste sted, den kan ende – det er én, der selv ved, hvor meget plads den har, og reagerer på det uden nogens hjælp. Media query var i to årtier CSS'ens eneste værktøj til responsivt design, så vi overbrugte det naturligvis også dér, hvor det egentlig handlede om forælderen, ikke om viewporten. Container Queries er ikke endnu en syntaktisk variant af den samme mekanisme – det er et andet spørgsmål, stillet det rette sted i DOM-træet, understøttet af containment, der garanterer, at svaret aldrig går i selvsving. Koden fra denne artikel – én komponent, én CSS-fil, nul JavaScript – virker korrekt i grid'et, i sidebaren og alle de andre steder, du endnu ikke ved, at du en dag vil sætte den ind.