How to Use CSS Container Queries With a Real Card Example
How CSS container queries work: container-type, @container syntax, cqi units, a copyable responsive card, browser support and fixes when they don't fire.

CSS container queries style an element based on the size of an ancestor you've marked as a container, instead of the size of the viewport. You opt an ancestor in with container-type: inline-size, then write @container (width > 700px) { ... } and the rules inside apply to that container's descendants whenever the condition holds. A card written this way adapts to a narrow sidebar or a wide main column without knowing which one it landed in.
Use them for component-level layout, where the question is "how much room does this element have?" Keep media queries for page-level decisions such as the overall grid, navigation, and anything that depends on the device rather than the slot. Both can live in the same stylesheet, and in most real projects they do.
When to use container queries instead of media queries#
A media query answers one question: how big is the viewport, or what else is true about the device. That's the right question for the page shell. It's the wrong question for a card that appears in three places on the same page, because at a single viewport width those three slots might be 280px, 600px and 1100px wide.
With media queries you end up writing slot-specific selectors like .sidebar .card, .main .card and .hero .card. Every new placement means another override, and every breakpoint change means touching all of them. With a container query the card carries its own rules, and the placement just supplies the space.
A rule of thumb I'd use:
- The style depends on the whole screen (layout columns, header behavior, print): media query.
- The style depends on the box the component was dropped into: container query.
- The style depends on a user preference such as reduced motion or color scheme: media query, since those aren't sizes.
MDN's container queries guide describes them as an alternative to media queries, not a replacement. That matches how they behave in practice.
Setting up a container with container-type and container-name#
Nothing happens until an ancestor declares a containment context. That's done with container-type, which takes three values:
| Value | What a query can measure | Containment applied |
|---|---|---|
inline-size |
The inline dimension (width in horizontal writing modes) | layout, style, inline-size |
size |
Both inline and block dimensions | layout, style, size |
normal |
Nothing size-related. This is the default. Still works for name-only and style queries | none for size |
The minimal setup is one line:
.post {
container-type: inline-size;
}
inline-size is the value you want almost every time. Width is what changes as a component moves between slots, and inline-size containment leaves the element free to grow vertically with its content.
Give the container a name when a component might sit inside several nested containers and you need to target a specific one:
.post {
container-type: inline-size;
container-name: sidebar;
}
The container shorthand sets both. Name comes first, then the type, separated by a slash:
.post {
container: sidebar / inline-size;
}
A container can also carry a name with no size type at all. It then answers only name-only queries, which is a tidy way to scope styles to "anything inside this region":
#container {
container-name: my-container;
}
@container my-container {
p {
background-color: lime;
padding: 0.5rem;
}
}
Only paragraphs inside #container get the lime background. Paragraphs outside are untouched.
How to write an @container rule#
The at-rule takes a condition and a block of ordinary rules. The condition needs a container name, a query, or both:
/* Nearest ancestor with a matching container-type */
@container (width > 400px) {
h2 {
font-size: 1.5em;
}
}
/* A specific named container */
@container tall (height > 30rem) {
p {
line-height: 1.6;
}
}
/* Name only, no size condition */
@container sidebar {
h2 {
background: blue;
}
}
Without a name, the size features are evaluated against the nearest ancestor that has a matching container-type. With a name, only containers carrying that name are considered. Range syntax with >, <, >= and <= works for the size features, and conditions combine with and, or and not.
You can't test two different containers in one condition. Nest instead. MDN's @container reference shows exactly this pattern:
@container summary (width > 400px) {
@container (width > 800px) {
/* summary is wider than 400px and an ancestor container is wider than 800px */
}
}
Two other condition types share the same at-rule. A style() query tests a computed value on the container, usually a custom property, as in @container style(--theme: dark). A scroll-state() query tests things like whether a sticky element is currently stuck or whether a scroller has more content in a direction, as in @container scroll-state(stuck: top). They're useful, but newer. The rest of this piece sticks to size queries, which is what most people typing this search are after.
Container query units: cqw, cqi and the rest#
Once an element has a size container somewhere above it, six length units become relative to that container:
| Unit | Meaning |
|---|---|
cqw |
1% of the container's width |
cqh |
1% of the container's height |
cqi |
1% of the container's inline size |
cqb |
1% of the container's block size |
cqmin |
The smaller of cqi and cqb |
cqmax |
The larger of cqi and cqb |
The typical use is fluid type that scales with the component rather than the screen. MDN's example sets a floor with max() so the heading never gets too small:
@container (width > 700px) {
.card h2 {
font-size: max(1.5em, 1.23em + 2cqi);
}
}
If there's no eligible container, these units fall back to the small viewport units for the same axis, so cqi behaves like svi. That's a quiet failure mode. The CSS is valid and renders, but the numbers track the window instead of the component.
It helps to see what a formula like that produces at a few container widths before committing to it. Assuming a 16px em:
// font-size: max(1.5em, 1.23em + 2cqi), with 1em = 16px
const em = 16;
const widths = [320, 480, 700, 900, 1200];
for (const inlineSize of widths) {
const cqi = inlineSize / 100; // 1cqi = 1% of the container's inline size
const px = Math.max(1.5 * em, 1.23 * em + 2 * cqi);
console.log(`${inlineSize}px container -> h2 font-size ${px.toFixed(2)}px`);
}
A responsive card you can copy#
Here's a card that stacks vertically when it's narrow and switches to a two-column layout with larger type when its container gives it room. The container is the list wrapper, not the card itself, because an element can't query its own size.
<div class="card-list">
<article class="card">
<img src="cover.jpg" alt="Cover art for the post">
<div class="card-body">
<h2>Card title</h2>
<p>Card content</p>
</div>
</article>
</div>
.card-list {
container: cards / inline-size;
}
.card {
display: grid;
gap: 1rem;
}
.card img {
width: 100%;
height: auto;
}
.card h2 {
font-size: 1em;
}
@container cards (width > 480px) {
.card {
grid-template-columns: 1fr 2fr;
align-items: center;
}
}
@container cards (width > 700px) {
.card h2 {
font-size: max(1.5em, 1.23em + 2cqi);
}
}
Drop .card-list into a 300px sidebar and the card stacks. Put it in a 900px main column and the image sits beside the text with a bigger heading. Same markup, same CSS, no slot-specific overrides. If your breakpoints live in a token set, the piece on design tokens with a real JSON example covers how to keep component breakpoints and page breakpoints from drifting apart.
This snippet simulates which blocks fire for a few typical slot widths, which is a quick sanity check when you're choosing thresholds:
const rules = [
{ label: "cards (width > 480px): two columns", test: (w) => w > 480 },
{ label: "cards (width > 700px): fluid heading", test: (w) => w > 700 },
];
const slots = { sidebar: 300, "main column": 760, hero: 1100 };
for (const [slot, width] of Object.entries(slots)) {
const matched = rules.filter((r) => r.test(width)).map((r) => r.label);
const result = matched.length ? matched.join("; ") : "base styles only (stacked)";
console.log(`${slot} at ${width}px -> ${result}`);
}
Querying height and other size details#
Height queries work, but only against a size container. inline-size containment measures the inline axis alone, so @container (height > 30rem) evaluated against an inline-size container never matches.
.panel {
container-type: size;
height: 100%; /* a size container needs a height from somewhere */
}
@container (height > 30rem) {
.panel-body {
overflow: visible;
}
}
The catch is size containment itself. A size container doesn't take its dimensions from its children, so one with no explicit or inherited height collapses to zero in that axis. Give it a height, or let a parent grid or flex layout size it, before querying it. This is the single most common reason height queries appear not to work.
The cqh and cqb units have the same dependency. They need a container with block-axis containment to resolve against anything other than the viewport fallback.
Browser support#
The @container at-rule is marked Baseline Widely available on MDN, with cross-browser support since February 2023. Size queries, named containers and the cq* units are fine for production today. MDN's page does note that some parts of the feature have varying support, which mostly concerns the newer style() and scroll-state() conditions. Check Can I use for container queries if you have to support older browsers or you lean on those newer parts.
For browsers without support, MDN's guide suggests building the card with grid and switching columns with a media query:
.card {
display: grid;
grid-template-columns: 2fr 1fr;
}
@media (width <= 700px) {
.card {
grid-template-columns: 1fr;
}
}
An unsupported @container block is simply ignored, so you can layer: write the media-query version first, then container queries that take over where they're understood. If you want the opt-in to be explicit, wrap the container-aware rules in @supports (container-type: inline-size).
Common mistakes when container queries aren't working#
- No container declared. The query is evaluated against the nearest ancestor with a matching
container-type. If there isn't one, nothing matches. Addcontainer-type: inline-sizeto a wrapper. - Styling the container itself. Rules inside
@containerapply to descendants of the container, not to the container. If the card's own outer box needs to change, wrap the card in a container element and query that. - Querying height on an inline-size container. Switch to
container-type: sizeand make sure the container actually has a height. - A size container with no height. Size containment means the element won't grow with its content. It needs a height from a parent layout or an explicit value.
- Wrong shorthand order. It's
container: name / type. Writingcontainer: inline-size / cardsis invalid and the declaration is dropped. - Name mismatch.
@container sidebar (...)only looks at containers namedsidebar. A typo in either place means the query silently never matches. - Expecting one query to test two containers. That's not possible in a single condition. Nest two
@containerrules instead. - Using cq units with no container above. They fall back to small viewport units, so the component looks like it's reacting to the window. Confirm an ancestor really has
container-type: sizeorinline-size. - Mixing up the inline axis with width.
cqiandinline-sizefollow the writing mode. In vertical writing modes the inline axis is vertical, socqiandcqwstop meaning the same thing.
Quick reference#
/* Declare a container */
.wrapper { container-type: inline-size; } /* width only; the usual choice */
.wrapper { container-type: size; } /* width and height; needs an explicit height */
.wrapper { container: cards / inline-size; } /* shorthand: name / type */
/* Query it (rules apply to descendants of the container) */
@container (width > 700px) { .child { } } /* nearest size container */
@container cards (width > 700px) { .child { } } /* named container */
@container cards { .child { } } /* name only */
@container (width > 400px) and (height > 30rem) { .child { } }
/* Units: 1% of the container */
/* cqw cqh cqi cqb cqmin cqmax */
.child { font-size: max(1.5em, 1.23em + 2cqi); }
Before you ship, run down this list:
- The wrapper, not the card, carries
container-type. - Width queries use
inline-size. Height queries usesizeplus a real height. - The name in
container-namematches the name in@containerexactly. - A media-query fallback covers browsers that ignore
@container, if you still support any. - Every
cq*unit has a size container somewhere above it.
If you go on to use scroll-state() queries for sticky headers, they pair naturally with the techniques in CSS scroll-driven animations. And when a container query flips a layout that's also animated, check which CSS properties are cheap to animate so the transition doesn't trigger layout on every frame.
Quick answers
What is a CSS container query?
It's a CSS rule that applies styles to the descendants of an element based on that element's size, name or computed styles, rather than the viewport. You mark the element with container-type, then write an @container block with the condition.
Can I use CSS container queries today?
Yes. MDN lists the @container at-rule as Baseline Widely available, supported across browsers since February 2023. Size queries and the cq units are safe for production; check Can I use before relying on the newer style() and scroll-state() conditions.
Why are my container queries not working?
Most often no ancestor has container-type set, or the rules are trying to style the container itself instead of its descendants. Height queries also fail against an inline-size container, and a size container with no explicit height collapses to zero.
Do container queries work with height?
Yes, but the container needs container-type: size, not inline-size, and it must get a height from an explicit value or a parent layout. Size containment stops it from sizing to its own content.
Can I use container queries with Tailwind CSS?
Tailwind's responsive design documentation covers its container query utilities; check that page for the current class names. The generated CSS is the same container-type and @container described here, so the same gotchas apply.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note