CSS container queries
CSS container queries let you style elements based on their parent container's size, not the viewport.
Theory
TL;DR
- Media queries watch the browser window; container queries watch the parent element.
- Analogy: media queries are like a central thermostat (the whole house reacts the same); container queries are like a personal thermostat on each device (adapts to its own shelf space).
- You need
container-type: inline-sizeon the parent before@containerdoes anything. - Use container queries for reusable components; use media queries for page-wide layouts.
- Browser support: Chrome 105+, Firefox 110+, Safari 16+.
Quick example
Two containers, different widths. Same .card inside each. Only the wide one triggers the layout change:
.parent {
container-type: inline-size; /* declares this as a query container */
}
@container (min-width: 400px) {
.card {
display: flex;
flex-direction: row; /* side-by-side only in wide containers */
}
}<div class="parent" style="width: 300px">
<div class="card">Narrow: stays stacked</div>
</div>
<div class="parent" style="width: 500px">
<div class="card">Wide: goes side-by-side</div>
</div>The card in the 300px container stays stacked. The card in the 500px container switches to row layout. Browser width changes nothing here.
Key difference
@media queries check window dimensions via matchMedia(). The whole page shares one viewport, so every component using @media is coupled to the page context. @container works per-element: the browser creates a query list for each declared container and evaluates it independently. The same .card can be narrow in a sidebar and wide in a hero section, with no extra CSS.
When to use
- Card component placed in both a sidebar and a main grid: container queries (each adapts locally).
- Dashboard widgets of varying widths: container queries (they evaluate independently of each other).
- Full-page nav collapse on mobile: media queries (this is a global layout decision).
- Print styles or orientation changes: media queries (device-level, not component-level).
The simplest rule: if the component gets reused across different layouts, container queries. If you are resizing the page structure itself, media queries.
Comparison table
| Feature | Media Queries | Container Queries |
|---|---|---|
| Trigger | Viewport (browser) size | Parent container size |
| Syntax | @media (min-width: 768px) | @container (min-width: 400px) |
| Scope | Global (whole page) | Local (per container) |
| Browser support | IE9+ | Chrome 105+, Firefox 110+, Safari 16+ |
| Best for | Page layouts, themes, print | Components in grids and flex layouts |
How it works internally
The browser creates a ContainerQuery object for each element with container-type declared. Under the hood, this works similarly to ResizeObserver: when the container's inline or block size changes, the engine re-evaluates the @container rule list and updates styles. If nothing changed, no reflow happens. The container also establishes a containment context, isolating the subtree from the parent layout before the query fires. That is why a container cannot query its own size: the spec prevents circular dependency at the containment boundary.
Container query units
Container queries ship with their own length units:
cqw— 1% of the container's widthcqh— 1% of the container's heightcqi— 1% of the container's inline sizecqb— 1% of the container's block sizecqmin/cqmax— smaller or larger ofcqiandcqb
.card-container {
container-type: inline-size;
}
.card {
font-size: clamp(1rem, 4cqw, 2rem); /* scales with container, not viewport */
padding: clamp(1rem, 5cqw, 3rem);
}For components that live inside other elements, this is more predictable than vw units.
Named containers
When you have multiple containers on a page, name them to control which @container query applies:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.main-content {
container-type: inline-size;
container-name: main;
}
@container sidebar (min-width: 300px) {
.card { /* fires only inside .sidebar */ }
}
@container main (min-width: 600px) {
.card { /* fires only inside .main-content */ }
}Without a name, @container queries the nearest ancestor with container-type.
Common mistakes
Forgetting container-type on the parent. This is the most common issue. Without a declared container, @container rules are silently ignored. No error, no warning, just nothing happening.
/* wrong: no container declared, @container is ignored */
@container (min-width: 400px) {
.card { display: flex; }
}
/* correct */
.parent {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { display: flex; }
}Using block-size when inline-size is what you need. container-type: block-size works but is rarely required and has spotty support (Firefox added it in 121). For most components, inline-size is enough. If you need to query both dimensions, use container-type: size.
Nesting containers in flex/grid without isolation. When a child container resizes, it can trigger the parent container's re-evaluation, which resizes the child again. Chrome 117+ added self-contained to break this loop:
.inner-container {
container-type: inline-size self-contained;
}Over-querying small containers. A @container (min-width: 300px) on a 20px icon element never fires. It wastes evaluation cycles. Check the container's actual minimum width before writing queries against it.
Assuming style queries work without declaring style in container-type. Container style queries (Chrome 111+) let you query custom property values, but style must be in container-type:
.card-container {
container-type: style inline-size; /* 'style' is required here */
}
@container style(--variant: dark) {
.card { background: #111; color: #fff; }
}Real-world usage
- Shadcn/UI — card components use
@containerso they adapt in any layout without per-context overrides. - Tailwind CSS v3.2+ — ships
container-typeutilities and an official@containerplugin. - Chakra UI — dashboard tiles in flex grids use container queries to stack or split based on tile width.
- Headless UI — modal contents resize based on the dialog container width, not the viewport.
I started using container queries after spending too much time fighting @media breakpoints in a component library where cards appeared in sidebars, grids, and full-width banners at the same time. One set of @container rules replaced three sets of layout-specific overrides.
Follow-up questions
Q: What is the difference between container-type: inline-size and size?
A: inline-size tracks only width. size tracks both width and height. Use size only when the query needs to respond to vertical changes too, since it creates stronger containment constraints and can affect layout more broadly.
Q: Why can a container not query its own size?
A: It would create a circular dependency: the element's size depends on its children, which depend on the query result, which depends on the element's size. The spec breaks this by requiring containment to be established before the query evaluates.
Q: How do container queries interact with CSS containment?
A: Declaring container-type implicitly applies contain: layout style (or contain: layout style size for the size type). This creates an independent subtree and prevents parent layout shift when the container's children change.
Q: Is there a polyfill?
A: No full polyfill exists. The feature relies on browser-level ResizeObserver internals. The workaround is a JS ResizeObserver that adds and removes classes, paired with class-based CSS rules as fallback.
Q: Performance: 100 container queries vs 100 media queries?
A: Both use observer-based evaluation internally. In practice, 100+ containers means 100+ observers. Pair with content-visibility: auto on off-screen sections to limit active observers at any given time.
Q (senior): In a grid of 50 identical cards, how would you reduce container query evaluation cost while also supporting style queries?
A: Declare container-type: style size self-contained on a shared ancestor instead of on each card individually. One observer covers the ancestor. Cards read the ancestor's --theme custom property via a style query. Per Chromium DevTools traces, this drops from 50 independent observers to one per ancestor group.
Examples
Two cards, one component, different containers
Same component, same CSS, different output based on parent width.
<style>
.parent {
container-type: inline-size;
border: 1px solid #ccc;
margin: 20px;
padding: 10px;
}
.narrow { width: 300px; }
.wide { width: 560px; }
.card {
background: #e8f4ff;
padding: 16px;
display: flex;
flex-direction: column; /* default: stacked */
gap: 12px;
}
@container (min-width: 450px) {
.card {
flex-direction: row; /* side-by-side when container is wide enough */
align-items: center;
}
}
</style>
<div class="parent narrow">
<div class="card">
<img src="avatar.jpg" width="48" height="48" alt="User">
<div>
<strong>Anna Smith</strong>
<p>Product designer</p>
</div>
</div>
</div>
<div class="parent wide">
<div class="card">
<img src="avatar.jpg" width="48" height="48" alt="User">
<div>
<strong>Anna Smith</strong>
<p>Product designer</p>
</div>
</div>
</div>Narrow parent (300px): image above text. Wide parent (560px): image left, text right. Browser window size is not involved.
Dashboard widget with named container and cqi units
A widget that appears in both a sidebar slot (240px) and a full-width panel (900px). No wrapper-specific CSS needed.
// DashboardCard.jsx
function DashboardCard({ title, value, trend }) {
return (
<div className="widget">
<div className="chart">
<h3 className="chart__title">{title}</h3>
<span className="chart__metric">{value}</span>
<span className="chart__trend">{trend}</span>
</div>
</div>
);
}.widget {
container-type: inline-size;
container-name: dashboard-card;
}
.chart {
padding: 16px;
display: grid;
grid-template-columns: 1fr; /* single column by default */
gap: 8px;
}
.chart__metric {
font-size: clamp(1.25rem, 6cqi, 2.5rem); /* fluid, tied to container inline size */
}
@container dashboard-card (min-width: 450px) {
.chart {
grid-template-columns: 1fr 2fr; /* chart and detail columns side by side */
}
}
@container dashboard-card (min-width: 600px) {
.chart__metric {
font-size: 2.5rem;
}
}The cqi unit scales the metric text relative to the container inline size, not the viewport. Resize the widget slot and the text scales with it.
Style queries for theme-aware components
Style queries (Chrome 111+) let a child read custom properties set on a container. No prop drilling, no extra class names.
.card-container {
container-type: style inline-size; /* 'style' enables style queries */
container-name: card-wrapper;
}
.card {
background: #fff;
color: #111;
padding: 20px;
border-radius: 8px;
}
@container card-wrapper style(--variant: dark) {
.card {
background: #1a1a2e;
color: #e0e0e0;
}
}
@container card-wrapper style(--variant: warning) {
.card {
background: #fff3cd;
color: #856404;
}
}<div class="card-container" style="--variant: dark">
<div class="card">Dark theme card</div>
</div>
<div class="card-container" style="--variant: warning">
<div class="card">Warning state card</div>
</div>
<div class="card-container">
<div class="card">Default card (no variant set)</div>
</div>The --variant custom property on the container acts as a signal. The card reads it without any JS. Skip style in container-type and the query never fires: all cards fall back to default styles.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.