Container Queries: komponenten som känner sin container, inte viewport
Du har ett produktkort. Bild ovanför titeln, pris, knappen "Lägg i kundvagn". I butikens huvudgrid, vid skärmbredder över 768px, växlar en media query till horisontell layout – bild till vänster, innehåll till höger, eftersom det utnyttjar den tillgängliga bredden bättre. Fungerar utmärkt. Sen vill produktteamet ha samma kort i den smala sidopanelen "rekommenderade produkter", bredvid artikelns huvudinnehåll. Du släpper in komponenten oförändrad – och den går sönder. Den horisontella layouten, designad för 700+ pixlar, försöker klämma in sig i en 260 pixel bred kolumn. Bilden pressas ihop till en list, texten radbryts till enstaka ord per rad.
Du har inte gjort något fel i CSS:en. Din media query fungerar exakt som den ska – den ställer bara fel fråga. @media (min-width: 768px) frågar: "hur bred är viewporten?". Produktkortet borde, för att se bra ut, aldrig bry sig om viewportens bredd – det borde bry sig om hur mycket plats det fick av sin förälder. Det är två olika frågor som CSS under två decennier har behandlat som en enda, eftersom det inte fanns något sätt att ställa den andra.
Varför media query inte räcker
Media query är en global fråga. Det spelar ingen roll om din komponent sitter i en 1200 pixel bred kolumn eller i ett smalt sidokort på 260 – @media (min-width: 768px) returnerar true i båda fallen, eftersom den frågar om webbläsarfönstret, inte om elementets förälder. En komponent byggd enbart på media queries är alltså bara korrekt i det sammanhang den ursprungligen designades för – och slutar vara korrekt så fort samma kod hamnar någon annanstans. Det bryter i grunden mot löftet som komponenter ska ge: att de går att återanvända säkert, var som helst i layouten.
Före 2023 var den enda verkliga lösningen ResizeObserver i JavaScript – du observerar ett element, mäter dess bredd vid varje förändring, sparar resultatet i state, lägger villkorligt till en CSS-klass. Det fungerar, men kostar: extra komponent-state, omrendering vid varje storleksändring, kod som måste skrivas för hand för varje enskild komponent, och som inte existerar förrän JavaScript har körts – så vid server-rendering visar första bilden ändå layouten "blint", innan ResizeObserver hinner mäta något.
jsx
// Lösningen före Container Queries – fungerar, men kostarimport { 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 samma problem utan en enda rad JavaScript, utan state och utan omrendering – för frågan "hur mycket plats har jag till mitt förfogande" hamnar tillbaka där CSS alltid har ställt den: i stilmallen.
Hur det egentligen fungerar under ytan: containment, inte bara ny syntax
Själva syntaxen @container ser ut som @media med ett annat namn – men skillnaden ligger någon annanstans, i vad som måste hända innan webbläsaren ens går med på att svara på en sådan fråga. För att ett element ska kunna vara en "query container" måste du deklarera det explicit:
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 är inte bara en brytare för query-läge – det är en deklaration av CSS Containment, en separat specifikation som säger till webbläsaren: "det här elementets innehåll påverkar inte dess storlek i inline-axeln, så du kan tryggt behandla det som en oberoende ö vid layoutberäkning". Det här är ingen oviktig implementationsdetalj – det är ett nödvändigt villkor för att container queries överhuvudtaget ska kunna existera utan att skapa en loop. Om webbläsaren tillät ett element att samtidigt ändra storlek som reaktion på sin egen bredd och påverka den bredden genom sitt innehåll, skulle ett cirkulärt beroende uppstå: innehållet ändrar containerns storlek → storleksändringen triggar @container → nya regler ändrar innehållet → innehållet ändrar containerns storlek igen. Containment bryter den loopen vid källan genom att tvinga fram att containerns storlek i en given axel bestäms oberoende av vad som finns inuti.
Det får en konkret, praktisk konsekvens vid val av container-type-värde:
inline-size– containment bara i inline-axeln (vanligtvis horisontellt). Elementets höjd bestäms fortfarande fritt av innehållet. Det är värdet du använder i 95 % av fallen – exakt som i exemplet med produktkortet.size– containment i båda axlarna samtidigt. Elementet slutar helt att förlita sig på sitt innehåll för att bestämma storlek – vilket betyder att du måste ge det en explicit höjd (t.ex. viaheightelleraspect-ratio), annars klipper containment bort dess naturliga höjd från innehållet och höjden blir effektivt noll.normal– standardvärdet, ingen containment, elementet är ingen query container.
container-name är valfritt, men värt vanan att använda – utan det frågar @container (min-width: 400px) den närmaste förfadern som är en container, vilken den än råkar vara. I en platt komponent är det ofarligt, men i en nästlad layout (ett kort inuti en panel, panelen inuti en kolumn – där både panelen och kolumnen är containers) pekar ett namngivet container entydigt ut exakt vilken förälder du frågar, i stället för att förlita dig på slumpmässig närhet i DOM-trädet.
Praktiskt exempel: ett kort, två kontexter, noll JavaScript
Vi återvänder till produktkortet från inledningen – den här gången byggt så att samma komponent renderas korrekt både i det breda griden och i den smala sidopanelen, utan att veta något om var den råkar hamna.
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ägg i kundvagn</button></div></article></div>);export default ProductCard;
css
/* Komponentens förälder deklarerar sig som query container */.product-card-slot {container-type: inline-size;container-name: product-card;}/* Standardlayout: bild ovanför innehåll – säkert i trånga utrymmen */.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;}/* Över 360px CONTAINER-bredd, inte viewport-bredd – växla till horisontell 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%;}}
Det avgörande beslutet ligger gömt i strukturen, inte i CSS:en: container-type sitter på .product-card-slot, alltså kortets förälder, inte på själva .product-card. Det är ingen slump eller överdriven försiktighet – specifikationen utesluter uttryckligen att ett element frågar om sin egen storlek via @container (samma cirkulära beroende-problem igen: ett element kan inte samtidigt definiera en container och stylas som reaktion på dess storlek). Därför är det typiska, återkommande mönstret en tunn wrapper-container utanpå och den faktiska komponenten inuti, stylad inom ett @container-block. Samma CSS-fil, insatt en gång i produktgriden (där sloten är 480px) och en gång i sidopanelen (där sloten är 240px), ger två olika, korrekta layouter – utan en enda prop, utan modifierarklass, utan en rad JavaScript.
Container-enheter: flytande utan viewport
Container Queries förde med sig en andra, mindre känd del av samma specifikation: enheter relativa till containern, inte viewporten – cqw (1 % av containerns bredd), cqh (1 % av höjden), cqi och cqb (1 % i inline-/block-axeln, alltså korrekta även i vertikalt skrivna språk), samt cqmin/cqmax, motsvarande vmin/vmax men beräknade utifrån containerns minsta respektive största dimension.
Deras naturliga användningsområde är flytande typografi inuti en komponent, oberoende av hur bred användarens skärm råkar vara:
css
.product-card__title {/* Skalar med kortets bredd, inte skärmens bredd */font-size: clamp(1rem, 0.85rem + 2cqi, 1.375rem);}
Skillnaden mot det redan kända clamp()-tricket med enheten vw är subtil men i praktiken avgörande: en titel skalad med vw ändrar storlek tillsammans med hela sidan, så två kopior av samma kort – en i griden på full bredd, en i den smala sidopanelen – får identisk fontstorlek, eftersom båda ser samma viewport. En titel skalad med cqi reagerar på det faktiska utrymme som just det kortet fått – i sidopanelen blir den mindre, i griden större, trots att viewporten hela tiden har exakt samma bredd.
Fallgropar och god praxis
container-type: sizeutan explicit höjd äter upp innehållet. Containment i block-axeln klipper bort elementets naturliga höjd från dess barn – anger du varkenheightelleraspect-ratiokrymper containern effektivt till nollhöjd, och dess barn försvinner visuellt, trots att de fortfarande finns i DOM. I de allra flesta fall räckerinline-size, och då uppstår inte problemet alls.- En container kan inte fråga om sig själv. Om du kämpar med att
@container"inte fungerar" är den första misstanken just detta:container-typeoch selektorn som stylas av@containersitter på samma element. Separera dem –container-typepå wrappern, målstilarna på barnet, precis som i exemplet med produktkortet ovan. - Container queries ersätter inte media queries – de kompletterar dem. Media query är fortfarande rätt verktyg för beslut på sidnivå (t.ex. om sidopanelen överhuvudtaget ska visas, eller om navigationen ska växla till hamburgermeny). Container query hanterar beslut på komponentnivå, oavsett var komponenten hamnar. En bra layout 2026 använder oftast båda samtidigt, var och en där frågan de ställer faktiskt passar.
- Gör inte varje div till en container "i förebyggande syfte". Containment har en verklig kostnad för renderingsmotorn – det är en signal om "behandla mig som en isolerad layoutenhet", användbar där du faktiskt har en komponent som reagerar på sin storlek, inte en standardinställning för hela DOM-trädet.
- Webbläsarstöd är inte längre ett problem.
container-typeoch@containernådde Baseline-status (brett tillgängligt) i början av 2023 – de fungerar nativt i Chrome, Safari och Firefox utan polyfills och utan@supports. Enheternacqw/cqioch resten av familjen har exakt samma täckning.
Sammanfattning
En återanvändbar komponent är inte en som har tillräckligt många props och modifierarklasser för att för hand hantera varje plats den kan hamna på – det är en som själv vet hur mycket plats den har, och reagerar på det utan någons hjälp. Media query var i två decennier CSS enda verktyg för responsivitet, så naturligtvis missbrukade vi det även där det egentligen handlade om föräldern, inte viewporten. Container Queries är inte ännu en syntaktisk variant av samma mekanism – det är en annan fråga, ställd på rätt plats i DOM-trädet, understödd av containment som garanterar att svaret aldrig loopar. Koden i den här artikeln – en komponent, en CSS-fil, noll JavaScript – fungerar korrekt i griden, i sidopanelen och på alla andra ställen du ännu inte vet att du en dag kommer att sätta in den.