CSS Container Queries: The Engineering Decision That Makes Components Truly Reusable
CSS Container Queries let components respond to their container instead of the viewport. Covers syntax, a worked example with fluid typography and theme-aware styling, and trade-offs: containment cost, Firefox unit gaps, and a migration path keeping media queries for page layout.
20 Jul 2026, 03:10 UTC

The Problem: Components That Don't Know Their Own Size
You build a card component. It looks great in the main content area at 800px wide. Then someone drops it into a 300px sidebar, and the layout breaks — text wraps awkwardly, images overflow, the grid doesn't collapse. The component has no idea it's in a sidebar because @media only sees the viewport.
This is the fundamental mismatch: media queries respond to the page, but components live in containers. CSS Container Queries (@container) fix this by letting a component query the size of its nearest ancestor with container-type set. The component becomes context-aware without JavaScript, without duplicate variants, and without the parent needing to pass down breakpoint classes.
How It Works: The Container Contract
Two pieces make this work. First, an ancestor opts in:
.sidebar { container-type: inline-size; }
inline-size (width only) is the default choice — it establishes a queryable container without the heavier layout containment of size (both dimensions). The element with container-type cannot be the same element being queried; the query always looks upward.
Second, the component writes its own breakpoints:
@container (min-width: 400px) {
.card { display: grid; grid-template-columns: 1fr 1fr; }
}
@container (max-width: 399px) {
.card { display: flex; flex-direction: column; }
}
When the sidebar resizes, the card rearranges itself. The same component works in a modal, a multi-column layout, or the main content area — each context provides its own container.
Worked Example: A Theme-Aware Card With Fluid Typography
Here's a complete pattern that combines size queries, style queries (custom property checks), and container query units. This card adapts its layout, theme, and type scale based entirely on its container.
<div class="layout">
<aside class="sidebar">
<article class="card">...</article>
</aside>
<main class="content">
<article class="card">...</article>
</main>
</div>
.sidebar { container-type: inline-size; container-name: sidebar; }
.content { container-type: inline-size; container-name: content; }
.card {
container-type: inline-size; /* nested container for inner elements */
padding: 1cqw; /* 1% of this card's width */
font-size: clamp(1rem, 2cqi + 0.5rem, 1.25rem);
}
/* Layout breakpoints scoped to each container */
@container sidebar (min-width: 350px) {
.card { display: grid; grid-template-columns: 1fr 2fr; gap: 1cqw; }
}
@container content (min-width: 600px) {
.card { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 2cqw; }
}
/* Theme via style query — no JS, no extra classes */
@container style(--theme: dark) {
.card { background: #1a1a1a; color: #e0e0e0; }
}
@container style(--theme: light) {
.card { background: #fff; color: #111; }
}
The sidebar and content areas each define their own container. The card queries its own container for fluid padding and type (cqw, cqi), and queries the named ancestor containers for layout breakpoints. Theme comes from a custom property on any ancestor: <div style="--theme: dark"> flips every card inside without a single class change.
Trade-offs You'll Hit in Production
Performance: Containment Has a Cost
container-type: inline-size adds layout containment. The browser must track the container's size and invalidate descendant styles on every resize. For a handful of components this is negligible; for a list of 500 virtualized rows, the style recalculation churn can show up in the Performance panel. Prefer inline-size over size unless you genuinely need height-based queries — size forces strict containment that also blocks margin collapsing and can shift absolutely positioned descendants.
Browser Support Baseline (2023) With Gaps
- Size/style queries: Chrome 105+, Firefox 110+, Safari 16+, Edge 105+
- Container query units (
cqw,cqi, etc.): Not yet shipped in Firefox stable (behindlayout.css.container-queries.enabledas of 2024). Verify caniuse.com before relying oncq*units in production. - Style queries: Chrome 111+, Firefox 113+, Safari 16.4+
Progressive enhancement with @supports (container-type: inline-size) is the safe path. The polyfill exists but adds runtime cost and doesn't support style queries.
The "Why Isn't This Working" Checklist
- Forgot
container-typeon an ancestor — the most common silent failure. - Tried to query the same element that has
container-type(queries look upward only). - Used a custom property for a style query that isn't declared on the container element itself (inherited values from higher ancestors are invisible to nested container style queries).
- Expected
cq*units to work in Firefox without the flag.
Migration Strategy: Duplicate, Don't Replace
You don't throw away media queries. Page-level breakpoints — navigation collapse, sidebar toggle, footer reflow — stay in @media. Components move to @container. The pragmatic migration:
- Identify components used in multiple layout contexts (cards, forms, tables, navigation items).
- Wrap each layout region (sidebar, main, modal, grid cell) with
container-type: inline-sizeand acontainer-name. - Copy existing
@mediabreakpoints into@containerrules for those components, using the same breakpoint values. - Test in each context. The component now works everywhere without variant classes.
For older browsers, the @media rules remain as fallback. The @container rules simply don't apply — no polyfill needed if the fallback layout is acceptable.
When to Reach for Container Queries
Use them when a component appears in more than one container width and its internal layout needs to change. Skip them for page-level structure, for components that only ever live in one context, or when the containment overhead outweighs the benefit (simple static cards, leaf components with no internal breakpoints).
Start with one reusable component — a card, a form field group, a data table — wrap its parent containers, and move its breakpoints from @media to @container. The payoff is immediate: the component becomes a true module that carries its own responsive logic wherever it's placed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.