css og layouts

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 koster
import { 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. via height eller aspect-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">
<img
src={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: size uten eksplisitt høyde spiser opp innholdet. Containment langs block-aksen kutter elementet fra barnas naturlige høyde – hvis du ikke oppgir height eller aspect-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 er inline-size nok, 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-type og selektoren som stylees av @container sitter på samme element. Skill dem – container-type på 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-type og @container oppnådde Baseline-status (bredt tilgjengelig) tidlig i 2023 – de fungerer nativt i Chrome, Safari og Firefox uten polyfills og uten @supports. Enhetene cqw/cqi og 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.