A media query only ever knows one thing: how wide the browser window is. That's a problem the moment a component gets reused somewhere its container isn't the full page — a sidebar widget, a dashboard tile that might span one grid column or three, a card dropped into a narrow panel on a wide desktop screen. The component has no way to know its own width; all it can ask is "how big is the whole window," which is often a completely different number.
Container queries fix this by letting an element query the size of its own container instead of the viewport. Mark an ancestor as a "query container" with one CSS property, and anything inside it can react to that element's width (or height) with an @container rule — the same component then looks right whether it's sitting in a 200px sidebar or a 600px main column, without a single line of JavaScript and without caring what the browser window is doing.
The 10 demos below are all real, draggable examples — literally: every one has a handle you drag to resize a box live and watch the CSS react in real time. Nothing here is a static screenshot of "before" and "after"; you're resizing the actual container and watching the actual rule fire.
Every embed also has a Fork & Edit button that opens the exact snippet, already running, in My Code — FWD Tools' free in-browser editor. Change a breakpoint number, delete a container-type line, and watch the reaction stop. That's a faster way to build real intuition for this feature than reading about it.
The syntax, in one breath
.card { container-type: inline-size; }
@container (min-width: 380px) {
.card { flex-direction: row; }
}
Two separate steps, and both are required. First, an element has to be explicitly turned into a query container with container-type — usually inline-size, which tracks the element's own width. Nothing queries anything until some ancestor has this. Second, an @container rule reacts to that container's size — and unlike @media, it applies to whichever container is nearest going up the tree, not to the whole page. The elements it styles inside the rule don't have to be the container itself; they can be any descendant, the same way demo #1 below styles a card based on its own width.
Why every demo here is something you drag, not click
Container queries are fundamentally about size, so a click-triggered demo would hide the actual point. Every box below has a real resize handle wired to genuine pointerdown/pointermove events that change an element's actual width (and, in one case, height) in pixels — not a CSS transform, not a simulated state. The @container rule shown under each demo is what's firing as you drag; the small JavaScript readout next to it only ever reports the current width and which side of a breakpoint it's on, the same "JS reports, CSS decides" split used throughout this site's demos, so what you're watching react is genuinely the container query, not a script pretending to be one.
1. Resizable Profile Card
Drag the handle on the right edge — the card switches from a stacked to a side-by-side layout once its own container crosses 380px, independent of how wide the browser window actually is.
The rule:
.cqCard { container-type: inline-size; }
@container (min-width: 380px) {
.cqCard { flex-direction: row; }
}
The card element itself isn't the query container here — its parent, .resizeBox, is (that's the box you're actually dragging). .cqCard just sits inside it and reacts, the same way any number of unrelated descendants could react to the same container at once. Before container queries, matching a profile card's layout to the width of whatever panel it landed in meant either a JavaScript ResizeObserver measuring the element by hand, or accepting that the card would look wrong in at least one context.
Best for: any card, panel, or widget that gets reused across differently-sized contexts — a dashboard, a CMS page builder, a component library. Tip: the container doesn't have to be the direct parent — any ancestor with container-type set works, however many plain divs sit in between.
2. Same Component, Two Different Containers
Identical HTML and CSS, dropped into two containers of different widths on the very same page, at the very same viewport width — the single clearest proof that this is genuinely about the container, not the screen.
The rule:
.navWidget { container-type: inline-size; }
@container (max-width: 200px) {
.navLabel { display: none; }
}
Nothing about the nav widget's own markup or CSS differs between the two panels — the only difference is which container it happens to be sitting inside. A media query applied to .navLabel could never produce two different results on the same page at the same time like this; it only has one number to check, the viewport, and that number is identical for both copies.
Best for: a component library where the same nav, card, or button genuinely gets embedded in unpredictable contexts — a CMS, a dashboard with resizable panels, an email-builder-style drag-and-drop layout. Tip: this is also why container queries matter for component libraries specifically — a component that queries itself, not the page, is the only kind that's honestly reusable.
3. Product Grid With Per-Card Layout
Drag the grid wider and watch closely: sometimes a wider grid flips a card's layout back to stacked, because more columns now fit and each one gets less room — a reaction happening per-card, which a page-level media query has no way to see.
The rule:
.prodCard { container-type: inline-size; }
@container (min-width: 210px) {
.prodCard { flex-direction: row; }
}
Each .prodCard is its own independent query container, sized by whatever column grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) happened to give it — not by the grid as a whole, and not by the viewport. That's the detail that makes the counter-intuitive part possible: as the overall grid widens past the point where a fourth column fits, every card's individual width drops, and cards that were previously wide enough to lay out sideways can flip back to stacked, at the exact same time the grid itself got bigger.
Best for: any responsive grid of cards — products, articles, team members — where a fixed number of grid breakpoints in a media query always feels one step behind the actual column count. Tip: pair this with grid-template-columns: repeat(auto-fill, minmax(...)) specifically — the container query then handles each card's internal layout while the grid itself handles how many columns fit, and neither needs to know about the other.
4. Container Query Units (cqw)
Drag the handle — the heading's font size is set in cqw, a unit meaning "percent of the container's width," not "percent of the viewport's width" the way vw does. The text scales with the box; your browser window stays completely out of it.
The rule:
.cqHeading {
font-size: clamp(16px, 9cqw, 48px);
}
cqw is one of a family of container query length units — cqw/cqh for percent of the container's width/height, cqi/cqb for the writing-mode-aware inline/block equivalents, and cqmin/cqmax for whichever of those is smaller or larger. Wrapping the unit in clamp() here does the same job it always does — it sets a floor and a ceiling so the text never gets unreadably small in a narrow box or absurdly large in a wide one, with the cqw value doing the actual scaling in between.
Best for: fluid typography and spacing inside a specific component — a card headline, a hero title inside a variable-width section — where vw-based fluid type would scale with the whole page instead of the one element that should actually control it. Tip: these units also work inside a calc() alongside fixed units, e.g. calc(12px + 2cqw), for scaling that never drops below a hard minimum.
5. Dashboard KPI Tile
Drag the handle wider — once the tile crosses 260px it reveals a trend line and description it was hiding, not just resizing a number. This is the pattern behind a dashboard whose widgets look intentional whether they occupy one grid column or three.
The rule:
.kpiDetail { display: none; }
@container (min-width: 260px) {
.kpiDetail { display: block; }
}
This is the same underlying mechanism as demo #1's layout flip, applied to a different kind of decision: instead of rearranging what's already visible, it's choosing whether to show extra content at all. A narrow tile stays honest about what it can actually fit — just the number and its label — rather than cramming in a trend line and a description that would wrap awkwardly or overflow.
Best for: dashboards, widget grids, or any layout where the same tile component can end up at genuinely different sizes depending on how a user arranges their view. Tip: this progressive-disclosure pattern (hide by default, reveal past a threshold) is usually the safer default compared to the reverse — it never risks a narrow container showing content that doesn't fit.
6. Named Containers
Drag the outer panel — two rules are watching two different named ancestors at once, one keyed to the inner card and one reaching straight past it to the outer panel, proving @container can target a specific ancestor rather than just "whichever is nearest."
The rule:
.outerPanel { container: panel / inline-size; }
.innerCard { container: card / inline-size; }
@container card (min-width: 200px) { .innerBadge { font-size: 15px; } }
@container panel (min-width: 420px) { .innerBadge { background: gold; } }
With no name, @container always resolves to the nearest ancestor container — which is fine until an element is nested inside more than one, and you need the outer one specifically. The container shorthand (name / type) assigns a name alongside the type, and @container <name> (...) then skips past any nearer, unnamed containers to find the one you actually meant. Both rules above are reading from the exact same drag gesture, since resizing the outer panel resizes the inner card proportionally along with it — they just answer to different names and different thresholds.
Best for: deeply nested component systems — a card inside a panel inside a page region — where more than one ancestor is a legitimate query container and "nearest" isn't always the one that should decide. Tip: name containers for what they represent (sidebar, modal, card) rather than generically — it turns @container rules into something genuinely readable months later.
7. Three-Tier Container Breakpoints
A settings row with three real container-query breakpoints, not just one — drag across the full range and watch the label, then the value column, appear as the panel actually has room for them.
The rule:
@container (min-width: 180px) { .rowLabel { display: inline; } }
@container (min-width: 320px) { .rowValue { display: inline; } }
Nothing stops a single container from being watched by any number of independent @container rules, each with its own threshold — exactly the way a page can have several @media breakpoints. Here that means a settings row can progressively reveal an icon, then a label, then a value column, rather than needing to jump straight from "minimal" to "everything" at one single width.
Best for: any information-dense row or list item — settings, table rows rendered as cards on narrow layouts, notification items — where more than two states genuinely exist between "cramped" and "spacious." Tip: keep breakpoint thresholds meaningfully spaced apart (as 180px and 320px are here) — two triggers close together can cause visibly janky, flickery reflows while dragging or resizing.
8. Height-Aware Container
Drag the corner handle. This box uses container-type: size instead of inline-size — it can query height too, so the description disappears once the box gets short, no matter how wide it stays.
The rule:
.noticeCard { container-type: size; }
@container (max-height: 120px) {
.noticeDesc { display: none; }
}
container-type: inline-size — used in every other demo in this batch — only ever tracks width. Querying height needs size instead, which contains layout on both axes. That comes with a real requirement: a size-containing element needs a genuinely definite height from somewhere else (an explicit height, or a parent that gives it one), because size containment tells the browser to ignore the element's children when computing its own size — without an explicit height, a size-contained box with no other height source can collapse to zero.
Best for: fixed-aspect boxes, dashboard tiles with an explicit grid-row height, or any card whose available height — not just width — genuinely varies and should drive what's shown. Tip: reach for inline-size by default and only use size when a height-based rule is a genuine requirement — it needs that explicit height, which most naturally-flowing content doesn't have and shouldn't be forced into.
9. Style Container Queries
Click the switch — it doesn't resize anything. It flips a custom property on the container, and @container style() reacts to that property's value instead of to a width or height.
The rule:
.densityBox { container-name: densitybox; }
@container densitybox style(--density: compact) {
.densityCard { padding: 8px 10px; }
.densityCard p { display: none; }
}
Every other demo here queries size — this one queries state, expressed as a CSS custom property. Setting --density: compact on the container (in this demo, via one line of JavaScript on a button click, but it could just as easily come from a data attribute–driven style rule) is what the style() condition checks; nothing about the container's width or height needs to change for it to match. Notice the container here doesn't need container-type: inline-size or size at all — a style query only needs the element to be a named container, since it isn't reading the container's dimensions.
Best for: a design-system "compact mode," theme, or density toggle that needs to cascade down to descendants without threading a class onto every single child manually. Tip: style queries currently only reliably support custom-property conditions like the one here — they shipped later than size-based container queries and browser support isn't as universal yet, so check caniuse.com/css-container-queries before relying on this one for anything load-bearing.
10. Media Query vs. Container Query, Side by Side
Drag the handle down to a narrow width. The top widget is styled with @media (min-width: 500px) — it only ever looks at your browser window, which never changes here, so it stays in its wide layout and overflows. The bottom widget uses @container and correctly adapts to the box it's actually in.
The rule:
@media (min-width: 500px) { .mediaWidget { flex-direction: row; } }
@container (min-width: 300px) { .containerWidget { flex-direction: row; } }
This is the demo that makes the whole article's point in one gesture. Both widgets sit inside the exact same box, so this is an honest, identical comparison — the only difference is which condition each one checks. The media query is answering a question that was never actually useful here ("how wide is the window"), and the container query is answering the one that was ("how wide is this") — and the visible overflow on the top widget as you narrow the box is exactly the failure mode container queries exist to prevent.
Best for: literally any component you're deciding whether to reach for a media query or a container query for — if the honest answer to "what size actually matters here" is the component's own box rather than the browser window, this demo is the reason to pick @container. Tip: media queries aren't obsolete — page-level decisions like overall layout direction or hiding a whole sidebar are still legitimately about the viewport. The two are complementary, not competing.
Common container query pitfalls
- Forgetting container-type means the query silently never fires. An
@containerrule with no ancestor carryingcontainer-typeanywhere above it doesn't error — it simply never matches, and the CSS inside it never applies. If a rule "isn't working," the very first thing to check is whether some ancestor actually opted in. - An element cannot query its own size.
container-typeestablishes the container on one element, but the@containerrule that reads it has to apply to a descendant — an element querying itself would be asking a question with no stable answer, since its own size could depend on the very rule deciding it. This is why every demo above setscontainer-typeone level up from whatever actually changes. - container-type: size needs a real, explicit size to contain. As demo #8 covers,
sizecontainment (unlikeinline-size) affects both axes and ignores children when sizing the box — an element with no explicit height and no other height source can collapse to zero rather than sizing to its content. - Container query units only work inside a sized container.
cqw/cqh/cqi/cqbresolve against the nearest ancestor with the matching containment — outside of any query container, they fall back to behaving like the small viewport units instead, which is easy to miss if a component gets moved outside its expected wrapper. - Broad browser support, but always worth a quick check. Size-based container queries (
container-type: inline-size/size, container query units) have shipped in every evergreen browser since 2023. Style queries are newer and less universally supported. Check caniuse.com/css-container-queries for the exact cutoffs your project needs.
Frequently asked questions
What are CSS container queries?
Container queries let an element apply styles based on the size (or, with style queries, a custom property's value) of an ancestor element — its "query container" — rather than the size of the browser viewport, which is what a traditional @media query is limited to. This lets a single component adapt correctly to whatever context it's actually placed in.
How do I set up a container query?
Two steps: mark an ancestor as a query container with container-type: inline-size (or size for height too), optionally giving it a name with container-name (or both at once via the container: name / type shorthand). Then write an @container (condition) { ... } rule targeting any descendant of that container.
What's the difference between container-type: inline-size and size?
inline-size only contains and tracks the element's inline dimension (width, in the default horizontal writing mode) — the right choice for the large majority of components, since it doesn't require the element to have an explicit height. size contains and tracks both axes, which is necessary if you need to query height, but requires the container to have a genuinely definite size on both axes from somewhere.
What are container query units?
cqw, cqh, cqi, cqb, cqmin, and cqmax are length units that resolve as a percentage of the nearest query container's width, height, inline-size, block-size, and the smaller/larger of those, respectively — the container-relative equivalent of vw/vh/vmin/vmax, which are always relative to the viewport instead.
Do container queries replace media queries?
No — they solve different problems. Media queries remain the right tool for page-level decisions that are genuinely about the viewport: overall layout direction, whether to show a full sidebar at all, print styles. Container queries are for a component that needs to adapt to its own box regardless of where that box ends up on the page, as demo #10 above demonstrates directly.
Are container queries well supported in browsers?
Size-based container queries and container query units have shipped in every evergreen browser (Chrome, Edge, Safari, Firefox) since 2023. Style queries arrived later and have less universal support. Check caniuse.com/css-container-queries for exact version cutoffs before depending on any of this for essential functionality in an older-browser context.
Every demo above ships its full HTML, CSS and JS in the HTML / CSS / JS tabs in its own top bar. But the better way to actually learn this is to click Fork & Edit instead of just copying: it hands you the same snippet in a live editor where you can change a breakpoint, swap inline-size for size, or delete the container-type line entirely and watch exactly what stops working. Once you've got a version you like, save it to My Code and it's yours to keep, tweak further, or reuse in a real project.
