I remember the exact moment it clicked. It was late. I had a lukewarm tea on the desk and a homepage hero that refused to behave at tablet breakpoints. Every tweak to a media query fixed one thing and nudged something else out of place. Then I did one tiny thing. I declared a container on the hero and wrote a single query against its width. The layout slid into place as if it had been waiting for me to ask the right question. Since then I have barely reached for viewport media queries in component work. Not because they are bad, but because there is finally a better fit for most of the problems we actually have.
Container queries let components respond to the space they live in rather than the size of the browser window. They are not a trick or a polyfill or a preprocessor dream. They are in the platform and widely available across modern browsers. If you have been holding off, you can ship this today with confidence.
The other good news is scope. Support for size based container queries is now over nine in ten users worldwide, and the ergonomics are pleasant once you get the hang of the couple of new properties. You give an element container status, usually the parent that wraps the component, then write a query with @container. That is it. No magic. Just the right level of control.
The tiny change that unlocks it
We start by telling the browser which element is a query container. The easiest way is the shorthand container, or use container-type plus an optional container-name if you want to target it explicitly.
HTML
<section class="hero">
<div class="hero__content"> <h1>Grow without the bloat</h1> <p>Simple pricing and no surprises.</p> <a class="cta">Get started</a> </div> <img class="hero__image" src="hero.jpg" alt=""> </section> CSS
/* Make the wrapper a size container so its children can query it */
.hero {
container: hero / inline-size; /* name + type in one line */
/* or: container-type: inline-size; container-name: hero; */
}
/* Base layout stacks content and image */
.hero__content { max-width: 60ch; margin-inline: auto; }
.hero { display: grid; gap: 1rem; }
/* When the hero has enough room, switch to a split layout */
@container hero (min-width: 52rem) {
.hero { grid-template-columns: 1fr 1fr; align-items: center; }
.hero__image { order: 2; }
}
No viewport breakpoints. No global assumptions. The hero behaves correctly wherever it is placed, including inside a narrow column on a content page. The trick is simply remembering to declare the container. You cannot query space you have not marked as queryable.
Rebuilding a hero without media queries
Let us make it a little more real. The brief is a familiar one. Big statement on the left when there is room, single column when there is not, and a quiet shift in typography so the headline never feels cramped.
.hero {
container: hero / inline-size;
display: grid;
gap: 1.2rem;
padding: 2rem;
}
.hero__content h1 {
font-size: clamp(1.75rem, 4vw, 3rem);
line-height: 1.1;
}
/* Split layout kicks in based on the hero space, not the viewport */
@container hero (min-width: 48rem) {
.hero { grid-template-columns: 1fr 1fr; gap: 2rem; }
}
/* Give the headline more presence only when the hero is wide enough */
@container hero (min-width: 62rem) {
.hero__content h1 { font-size: clamp(2.25rem, 3vw, 4rem); }
}
You can drop this hero into a sidebar layout, a full width landing page, or a third of a dashboard card and it adapts with zero extra work. That sense of calm you feel while reading the code is why teams fall hard for this approach.
If you ever forget the syntax or want to check what counts as a valid container type, the MDN pages on container and container-type are excellent quick references, and they include the baseline note that this has been broadly available since early 2023.
Cards and grids that respect their slot
Cards are where container queries shine. You define a card list as the container, then let each card adjust its inner layout when it gets enough room.
HTML
<ul class="product-list">
<li class="card">
<img class="card__img" alt="">
<h3 class="card__title">Starter</h3>
<p class="card__copy">All the basics for small teams</p>
<a class="card__cta">Choose plan</a>
</li>
<!-- more cards -->
</ul>
CSS
.product-list {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
container: cards / inline-size;
}
.card {
border: 1px solid oklch(0.9 0 0);
padding: 1rem;
display: grid;
gap: 0.75rem;
}
/* Upgrade the card layout when the list gives it more room */
@container cards (min-width: 62rem) {
.card { grid-template-columns: 8rem 1fr; align-items: start; }
.card__img { width: 100%; aspect-ratio: 1; object-fit: cover; }
}
Notice that we are not making assumptions about the viewport at all. The grid can live in a narrow column and still look great.
Container query units for effortless scale
There is a lovely extra once you are comfortable. Container query units give you lengths tied to the container itself. That means fluid typography or spacing that scales with the component’s actual slot, not the window.
CSS
/* cqi is the inline size unit, cqb is the block size unit */
.pricing { container: pricing / inline-size; }
/* Headline grows with its container but stays sensible */
.pricing__title { font-size: clamp(1.25rem, 6cqi, 2.25rem); }
/* Padding breathes on large cards but never feels wasteful */
.plan { padding: clamp(1rem, 4cqi, 2rem); }
Support for container units is strong on modern engines, and the units include cqw, cqh, cqi, cqb, cqmin, and cqmax. If you prefer a friendly explainer before you dive into a table, the CSS Tricks guide is a good tour that maps units to intuitive mental models.
Common gotchas and simple fixes
I tripped over the same handful of things at the start. You might too.
First, nothing works until you establish a container. Add container-type: inline-size to the parent you want to query, or use the shorthand. Second, queries only test ancestors that are containers, so you may want a wrapper that exists purely to provide a clean boundary for a component. Third, naming helps. If you give the container a name, you can compose multiple queries with clarity when you nest components, like @container sidebar (min-width: 40rem) { … }. The MDN reference spells this out with small, copyable examples.
Fourth, do not confuse container queries with media queries. Media queries still shine for big page level changes, such as repositioning the entire sidebar at certain viewport sizes. Container queries live lower in the tree where components need to be resilient in many contexts. The MDN article on container queries is a good place to refresh that difference if you have been away from CSS for a while.
A tiny side door with modern selectors and nesting
Once you start thinking in components, a few modern CSS features become obvious companions. The :has() pseudo class lets you style a parent based on what it contains. Think of an input group that changes the wrapper border when the inner input is invalid, or a card that tweaks spacing if it contains an image. This is more than a parent selector party trick and pairs beautifully with container logic.
And if you have not tried native CSS nesting yet, it is worth a go. It keeps related rules together with less repetition and works across modern browsers with very good coverage. The MDN usage guide shows where to place the ampersand for clarity when you need it.
Debugging without drama
You can inspect and debug container queries in Chrome DevTools. There is a specific panel section that shows which containers apply and when queries match, which saves a lot of guesswork when a nested component behaves in a surprising way. Once you have seen the visual overlay mark the container edges, you will not want to debug without it. Chrome for Developers
Performance and care notes
Container queries themselves are not a performance tax. They encourage cleaner boundaries which often reduces layout thrash. In practice I have seen simpler CSS, far less duplication, and fewer fragile overrides. If you add motion on top, remember our duty of care. Respect user preferences for reduced motion, and lean on the platform for transitions rather than heavy libraries. If you are curious, the View Transition API is a gentle way to add wayfinding without turning your site into a carnival ride.
Copy and paste starters
A few snippets you can use as a base today.
Named container with a couple of breakpoints
CSS
.section {
container: section / inline-size;
display: grid;
gap: 1rem;
}
@container section (min-width: 42rem) {
.section { grid-template-columns: 1fr 1fr; }
}
@container section (min-width: 64rem) {
.section { grid-template-columns: 2fr 1fr; }
}
Component that switches layout inside a wider container