Container queries are not magic. They are a constraint.
Container queries solve real problems. They also create new ones.
On my last three production projects I had the same experience: the first week was “wow, finally”, the second week was “why is this component suddenly huge and broken in this random layout”.
So this is not a tutorial. I am not going to re-explain what container queries are. If you know how media queries work and you roughly know what container-type does, you are good.
This is the rubric I now use on every component. I follow it pretty ruthlessly. It came out of three real builds:
- A marketing site with a visual page builder for a B2B SaaS.
- A design-heavy personal site (this one) with lots of experimental layouts.
- A data-heavy dashboard with nested sidebars and cards.
Across those three, I learned where container queries earn their keep, and where they are pure slowdown.
The one decision that comes before everything
I ask one question first, before I touch CSS:
“Does this component’s layout depend on the page, or on its parent?”
If the answer is “page”, it is a viewport component. It responds to the overall screen or app shell. I default to media queries.
If the answer is “parent”, it is a contextual component. It responds to the size or mode of its container. I default to container queries.
Ninety percent of my decisions are that simple. The trick is being honest about which bucket a component really belongs to.
Project 1: Marketing site with a page builder
This was a SaaS marketing site where non-devs could compose pages out of content blocks. Think hero, feature grid, logo wall, testimonial slider, pricing rows.
Marketing wanted freedom. They wanted to put a feature grid inside a nested column layout one day, then full-width the next week, then shove it into a sidebar for a landing page experiment.
If I had done this with only media queries, I would have created the usual mess:
- Classes like
.feature-grid--sidebarand.feature-grid--wide. - Random overrides in page-specific stylesheets.
- Every new layout needing yet another “just for this page” breakpoint tweak.
Instead I leaned hard into container queries here. But not everywhere.
What got container queries
Two patterns really wanted container awareness.
Reusable content blocks
Example: a <FeatureGrid> component that could be:
- 3 columns in a wide hero.
- 2 columns inside a narrow content column.
- 1 column in a sidebar or mobile layout.
I did this:
.feature-grid-container {
container-type: inline-size;
}
@container (min-width: 900px) {
.feature-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@container (min-width: 600px) and (max-width: 899px) {
.feature-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@container (max-width: 599px) {
.feature-grid {
grid-template-columns: 1fr;
}
}
The outer wrapper got the container. The inner grid just cared about that container’s width.
It meant the block adapted correctly wherever marketing dropped it. No page-specific media queries. No “special case” classes.
Components inside arbitrary columns
The page builder used a grid-of-grids pattern. You could nest columns two or three levels deep.
Anything that went inside those columns used container queries for structural layout. Cards, testimonial strips, logo walls. If the column was narrow, the component stacked. If it was wide, it spread out.
This is where container queries feel like cheating. They match how you think about the UI. “If my slot is this big, do X.”
What stayed on media queries
Not everything deserved container logic.
Site-wide layout pieces still followed viewport size:
- Header navigation (desktop vs mobile nav patterns).
- Global footer layout.
- Global paddings, section spacing, base type scales.
Those sit above the page builder and do not move between parents. They are inherently viewport components. I used plain media queries on body, main, header, footer level classes.
My rule here was simple: if moving this component to a different parent is not a supported feature of the system, it should not react to that parent. It should just react to the screen.
Project 2: My own site with fancy layouts
My personal site is where I try things that would get me funny looks in a client handoff.
Lots of overlapping sections. Asymmetric grids. Cards that sometimes behave like full-width sections and sometimes like small tiles.
When I first refactored it for container queries, I went too far. You can absolutely over-abstract this stuff.
How I broke it with overuse
I did a “pure container queries” experiment. No media queries for components. Just a couple for global type sizing.
Everything got container-type: inline-size;. Articles, sidebars, cards, galleries, even the page wrapper itself.
The result:
- Debugging layout issues meant checking which container was actually being matched.
- Deeply nested components would pick up weird intermediate widths.
- I constantly hit that feeling of “where is this coming from” in DevTools.
Technically it worked. Practically it was fragile.
The big problem: I had no hierarchy. Every container was treated as equally important, which is not how I actually think about my layout when I design it.
The hierarchy rule I use now
I rebuilt it with a very specific rule:
Only two container levels are allowed:
- Page containers for main content areas.
- Component containers for reusable blocks inside those areas.
Nothing inside a component is allowed to define a new container for layout. Typography and spacing variations are allowed to be plain descendants again.
Concretely, that looks like this:
.page-mainis a container. It controls how the primary content layout reacts to width.- Components like
.article-list,.gallery,.calloutdefine their own container scope inside.page-main. - Child pieces such as
.article-cardor.gallery-itemare not containers. They just respond to their parent component’s container queries.
This does two things for me:
- When something behaves weirdly, I have exactly two possible places to look for container rules.
- Design-wise, I am forced to think in two scales: page layout and block layout. No in-between mush.
I think this is the missing piece in most container query examples. The feature is powerful, but you still need a layout hierarchy or it becomes unreadable.
Project 3: A data-heavy dashboard
The third project was a dashboard with lots of cards, nested sidebars, resizable panels. Basically a layout minefield.
Here I saw the clearest split between when container queries shine and when they just complicate things.
Where container queries were absolutely worth it
Three patterns paid off immediately.
Cards that move between zones
We had cards that could appear:
- In a main 3-column grid.
- In a 2-column context sidebar.
- As a full-width row in a detail view.
Users could also resize side panels. So a card could be suddenly squeezed to a weird in-between width.
Viewport-based breakpoints are almost useless here. The viewport does not tell you how wide that middle column actually is.
So cards got container queries for their internal layout. Things like:
- Switching from table layout to stacked rows.
- Showing or hiding certain metadata.
- Reflowing actions from inline to bottom-aligned buttons.
Cards did not care about the total screen. They cared about their slot. Perfect container query territory.
Resizable split panes
We had a classic “sidebar + main + optional inspector” three-panel split, with drag handles.
Each pane had content that needed to adjust based on the pane width, not the viewport.
Those panes each became containers. Inside them, I used container queries to flip layouts and scale typography slightly for readability. Viewport queries had no idea what was going on inside.
Where I intentionally stuck with media queries
Then there are things that sound like container query use cases, but I think they are traps.
Global navigation modes
Side navigation vs top navigation vs hidden behind a hamburger: that is an app-level decision.
The behavior depended on available overall horizontal space and the presence of certain global panels. So I kept that at viewport level:
- Large screens: side nav open, inspector optional.
- Medium: collapsible side nav.
- Small: top nav and overlays.
Could I have hacked this through container queries using different layout wrappers as containers? Sure. It would be clever. It would also be impossible to reason about a year later.
Global typography and rhythm
I keep base font sizes, line heights, and vertical rhythm on media queries.
Container queries are great for component-level typography tweaks, like smaller labels in a narrow card. They are terrible for global type scales. That belongs to the viewport and design system level, not to whatever random parent happens to wrap some content.
The rubric I actually use now
Here is what my mental decision tree looks like when I build a new component.
Step 1: Can this move?
I ask: Will this component be used in multiple layouts or regions?
- If yes, I assume it will end up in containers with different widths. It gets a container and container queries for its internal layout.
- If no, it probably belongs to a specific region such as header, footer, main, sidebar. I stick with media queries until proven otherwise.
Step 2: What actually drives its behavior?
I ask: Does the layout change depend on the screen, or on the parent width?
- If the component should behave differently on phones vs desktops regardless of parent width, I use media queries.
- If the component just needs “small / medium / large slot” behavior, I use container queries keyed off the container width.
I force myself to pick one primary driver. Hybrid logic is how you end up with gnarly condition stacks that nobody wants to debug.
Step 3: Where does it sit in the hierarchy?
I decide if it is:
- A page-level region (header, main, sidebar, footer), or
- A reusable block inside those regions, or
- A leaf item such as a card row, form field, or icon.
Then I apply this rule:
- Page-level regions: viewport media queries only.
- Reusable blocks: allowed to define containers and use container queries.
- Leaf items: no containers, keep styles simple and rely on parent decisions.
This hierarchy is the only way I have found to stop container queries from turning into an “everything is relative to everything” mess.
Step 4: Can I debug this in 30 seconds?
I run a quick sanity check: If this layout breaks, can I find the cause in under 30 seconds?
If I have to mentally simulate two or three nested containers and a stack of viewport breakpoints, I back off. Usually that means:
- Removing a nested container I do not really need.
- Collapsing some breakpoints into fewer, broader ranges.
- Moving a layout decision either fully to the viewport level or fully to the container level.
If you cannot debug it fast, future you will hate you for being clever.
How this changes my defaults
This is the honest part. My default is still media queries.
For a basic marketing page without a builder, I barely touch container queries. Most things are fixed regions. They scale with viewport size. Life is simple.
As soon as the layout starts to look like “Lego blocks you rearrange”, or “cards flying between different zones”, or “panels you can resize”, container queries come off the bench and become first-class.
I do not treat container queries as a modern replacement for media queries. I treat them as a second axis. Viewport for global behavior. Container for slot behavior.
If you are early in adopting them, I would start with one category: cards. Make your cards container-aware and keep everything else viewport-based. You will feel the difference, without flooding your codebase with container declarations you regret later.
Then apply the same question I keep asking myself on every new component: does this care about the page, or about its parent? The answer usually writes the CSS for you.
Subscribe to my newsletter to get the latest updates and news
Member discussion