Container Queries: komponenten som kjenner sin egen beholder, ikke viewporten
Du har et produktkort. Bilde over tittelen, pris, knappen «Legg i handlekurv». I butikkens hovedrutenett, ved skjermbredde over 768px, bytter en media query det til et horisontalt oppsett – bilde til venstre, innhold til høyre, fordi det utnytter den tilgjengelige bredden bedre. Fungerer utmerket. Så vil produktansvarlig ha det samme kortet i en smal sidebar med «anbefalte produkter», ved siden av artikkelens hovedinnhold. Du limer inn komponenten uendret – og den bryter sammen. Det horisontale oppsettet, designet med tanke på 700+ piksler, prøver å presse seg inn i en 260-piksler bred kolonne. Bildet klemmes sammen til en strimmel, teksten brytes ned til enkeltord per linje.
Du har ikke gjort noen feil i CSS-en. Media query-en din fungerer akkurat som den skal – den svarer bare på feil spørsmål. @media (min-width: 768px) spør: «hvor bred er viewporten?». For at produktkortet skal se bra ut, burde det aldri bry seg om viewportens bredde – det burde bry seg om hvor mye plass det fikk tildelt av forelderen. Dette er to forskjellige spørsmål som CSS i to tiår har behandlet som ett, fordi det ikke fantes noen måte å stille det andre spørsmålet på.
Hvorfor media query ikke er nok
En media query er en global spørring. Det spiller ingen rolle om komponenten din sitter i en kolonne på 1200 piksler eller i et smalt sidekort på 260 – @media (min-width: 768px) returnerer samme true i begge tilfeller, fordi den spør om nettleservinduet, ikke om elementets forelder. En komponent bygget utelukkende på media queries er dermed bare korrekt i konteksten den opprinnelig ble designet for – og slutter å være korrekt når den samme koden havner et annet sted. Dette bryter grunnleggende med det løftet komponenter er ment å gi: at de trygt kan gjenbrukes hvor som helst i layouten.
Før 2023 var den eneste reelle løsningen ResizeObserver i JavaScript – du observerer elementet, måler bredden ved hver endring, dytter resultatet inn i state, og legger betinget til en CSS-klasse. Det fungerer, men koster: ekstra komponent-state, re-rendering ved hver størrelsesendring, kode du må skrive manuelt for hver eneste komponent, og som ikke eksisterer før JavaScript har kjørt – så ved server-side rendering vil den første renderingen uansett vise layouten «blindt», før ResizeObserver rekker å måle noe som helst.
jsx
// Løsningen fra før Container Queries – fungerer, 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 problemet uten en eneste linje JavaScript, uten state og uten re-rendering – fordi spørsmålet «hvor mye plass har jeg til rådighet» havner tilbake der CSS alltid har stilt det: i stilarket.
Hvordan det faktisk fungerer under overflaten: containment, ikke bare ny syntaks
Selve syntaksen @container ser ut som @media med et annet navn – men forskjellen ligger et annet sted: i det som må skje før nettleseren i det hele tatt godtar å svare på en slik spørring. For at et element skal kunne være en «spørringsbeholder», må du eksplisitt 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 bryter som slår på spørringsmodus – det er en deklarasjon av CSS Containment, en egen spesifikasjon som forteller nettleseren: «innholdet i dette elementet påvirker ikke størrelsen dets langs inline-aksen, så du kan trygt behandle det som en uavhengig øy under layout-beregningen». Dette er ikke en betydningsløs implementasjonsdetalj – det er en nødvendig forutsetning for at container queries i det hele tatt skal kunne eksistere uten å ende i en evig løkke. Hvis nettleseren tillot et element å både endre størrelse som reaksjon på sin egen bredde og påvirke den bredden gjennom sitt eget innhold, ville det oppstått en sirkulær avhengighet: innholdet endrer beholderens størrelse → størrelsesendringen utløser @container → de nye reglene endrer innholdet → innholdet endrer beholderens størrelse på nytt. Containment kutter denne løkken ved roten, ved å tvinge frem at beholderens størrelse i en gitt akse fastsettes uavhengig av det som er inni.
Dette har en konkret, praktisk konsekvens for valget av container-type:
inline-size– containment kun langs inline-aksen (vanligvis horisontalt). Elementets høyde følger fortsatt fritt av innholdet. Dette er verdien du griper til i 95 % av tilfellene – akkurat som i eksempelet med produktkortet.size– containment langs begge aksene samtidig. Elementet slutter helt å stole på sitt eget innhold når størrelsen fastsettes – noe som betyr at du må gi det en eksplisitt høyde (f.eks. viaheightelleraspect-ratio), ellers kutter containment det fra innholdets naturlige høyde, og høyden faller effektivt til null.normal– standardverdien, ingen containment, elementet er ikke en spørringsbeholder.
container-name er valgfritt, men verdt å gjøre til vane – uten det spør @container (min-width: 400px) mot den nærmeste forfaren som er en beholder, uansett hvilken det måtte være. I en flat komponent er dette ufarlig, men i et nøstet oppsett (et kort inni et panel, panelet inni en kolonne – der både panelet og kolonnen er beholdere) forteller en navngitt beholder entydig hvilken forfar du faktisk spør etter, i stedet for å stole på tilfeldig nærhet i DOM-treet.
Praktisk eksempel: ett kort, to kontekster, null JavaScript
Tilbake til produktkortet fra innledningen – denne gangen bygget slik at den samme komponenten rendrer korrekt både i det brede rutenettet og i den smale sidebaren, uten noen som helst kunnskap om hvor den faktisk er plassert.
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">Legg i handlekurv</button></div></article></div>);export default ProductCard;
css
/* Komponentens forelder erklærer seg selv som spørringsbeholder */.product-card-slot {container-type: inline-size;container-name: product-card;}/* Standardoppsett: bilde over innhold – trygt i smal plass */.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å BEHOLDEREN, ikke viewporten – bytt til horisontalt oppsett */@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 avgjørende beslutningen ligger skjult i strukturen, ikke i CSS-en: container-type sitter på .product-card-slot, altså kortets forelder, ikke på selve .product-card. Dette er verken tilfeldig eller unødvendig forsiktighet – spesifikasjonen utelukker eksplisitt muligheten for at et element spør om sin egen størrelse via @container (samme sirkulære avhengighetsproblem igjen: et element kan ikke samtidig definere beholderen og bli stylet som reaksjon på dens størrelse). Derfor er det vanlige, gjentakende mønsteret en tynn wrapper-beholder utenpå og selve komponenten inni, stylet inne i en @container-blokk. Den samme CSS-filen, sluppet inn i produktrutenettet (der sloten er 480px) og i sidebaren (der sloten er 240px), gir to forskjellige, korrekte oppsett – uten en prop, uten en modifikator-klasse, uten en eneste linje JavaScript.
Beholderenheter: flyt uten viewport
Container Queries brakte med seg en annen, mindre kjent del av den samme spesifikasjonen: enheter relative til beholderen, ikke viewporten – cqw (1 % av beholderens bredde), cqh (1 % av høyden), cqi og cqb (1 % langs inline-/block-aksen, altså også korrekt for vertikalt skrevne språk), samt cqmin/cqmax, analoge til vmin/vmax, men regnet ut fra beholderens minste eller største dimensjon.
Deres naturlige bruksområde er flytende typografi inni komponenten, uavhengig av hvor bred brukerens skjerm akkurat er:
css
.product-card__title {/* Skalerer med kortets bredde, ikke skjermens bredde */font-size: clamp(1rem, 0.85rem + 2cqi, 1.375rem);}
Forskjellen fra det allerede kjente clamp()-trikset med vw-enheten er subtil, men i praksis fundamental: en tittel skalert med vw endrer størrelse sammen med hele siden, så to kopier av samme kort – én i rutenettet på full bredde, én i den smale sidebaren – vil få identisk skriftstørrelse, fordi begge ser den samme viewporten. En tittel skalert med cqi reagerer på den faktiske plassen som er tildelt det konkrete kortet – i sidebaren blir den mindre, i rutenettet større, selv om viewporten hele tiden har nøyaktig samme bredde.
Fallgruver og gode praksiser
container-type: sizeuten eksplisitt høyde spiser opp innholdet. Containment langs block-aksen kutter elementet fra barnas naturlige høyde – hvis du ikke oppgirheightelleraspect-ratio, krymper beholderen effektivt til null høyde, og barna forsvinner visuelt, selv om de fortsatt finnes i DOM-en. I de aller fleste tilfeller erinline-sizenok, og dette problemet oppstår ikke.- En beholder kan ikke spørre seg selv. Hvis du sliter med at
@container«ikke virker», er den første mistanken nettopp dette:container-typeog selektoren som stylees av@containersitter på samme element. Skill dem –container-typepå wrapperen, målstilene på barnet, akkurat som i eksempelet med produktkortet ovenfor. - Container queries erstatter ikke media queries – de utfyller dem. Media query er fortsatt riktig verktøy for beslutninger på side-nivå (f.eks. om en sidebar i det hele tatt skal vises, eller om navigasjonen skal bytte til en hamburgermeny). Container query tar seg av beslutninger på komponent-nivå, uansett hvor komponenten havner. God layout i 2026 bruker som regel begge samtidig, hver der spørsmålet den stiller faktisk passer.
- Ikke gjør hver eneste div til en beholder «i tilfelle». Containment har en reell kostnad for rendringsmotoren – det er et signal om «behandle meg som en isolert layoutenhet», nyttig der du faktisk har en komponent som reagerer på sin egen størrelse, ikke en standardinnstilling for hele DOM-treet.
- Nettleserstøtte er ikke lenger et problem.
container-typeog@containeroppnådde Baseline-status (bredt tilgjengelig) tidlig i 2023 – de fungerer nativt i Chrome, Safari og Firefox uten polyfills og uten@supports. Enhetenecqw/cqiog resten av familien har nøyaktig samme dekning.
Oppsummering
En gjenbrukbar komponent er ikke en som har nok props og modifikator-klasser til manuelt å håndtere hvert eneste sted den kan havne – det er en som selv vet hvor mye plass den har, og reagerer på det uten hjelp fra noen. Media query var i to tiår det eneste CSS-verktøyet for responsivitet, så vi misbrukte det naturlig nok også der spørsmålet egentlig gjaldt forelderen, ikke viewporten. Container Queries er ikke enda en syntaktisk variant av samme mekanisme – det er et annet spørsmål, stilt på riktig sted i DOM-treet, støttet av containment som garanterer at svaret aldri havner i en løkke. Koden fra denne artikkelen – én komponent, én CSS-fil, null JavaScript – fungerer korrekt i rutenettet, i sidebaren og på ethvert annet sted du ennå ikke vet at du en dag kommer til å plassere den.