CSS Container Queries: Build Truly Responsive Components

CSS container queries
(@container) style components based on the width of their parent container instead of the browser viewport. Add container-type: inline-size to a parent element, write @container (min-width: 400px) { ... } rules on the children, and those children automatically adapt their layout based on the space available to them - not the screen size. All major browsers have supported container queries since early 2023 (Chrome 105+, Firefox 110+, Safari 16+), and as of 2026 they sit at over 96% global support according to Can I Use
.
The Problem Media Queries Cannot Solve
Media queries have been the backbone of responsive web design since 2012. They work well when a component always occupies the full viewport width, but they fall apart the moment a component gets placed in a context with less available space.
Consider a .card component designed with media queries. At viewports wider than 768px, it displays a horizontal layout with the image on the left and text on the right. This works perfectly in a full-width content area. But drop that same card into a 300px sidebar, and the “desktop” styles still apply because the viewport is still 1200px wide. The card overflows, text wraps in awkward places, and images are oversized for the space.
The traditional workarounds are all fragile. BEM modifier classes like .card--sidebar and .card--main hardcode layout-specific variants. JavaScript ResizeObserver solutions watch container widths and toggle CSS classes at runtime. Some teams write duplicate CSS rules for each layout context where the component might appear.
Every one of these approaches creates tight coupling between the component and its surroundings. A design system card component needs to work in a 12-column grid, a 4-column grid, a sidebar, a modal, and a full-width hero section. With media queries, you either maintain a growing list of layout-specific variants or accept that the component only looks right in one context.

Container queries fix this at the CSS level. The component itself declares how it should look at different available widths. Move the same card from a sidebar to a main content area and it automatically adapts without class changes, JavaScript, or layout-specific overrides. Instead of asking “how wide is the screen?”, the component asks “how much space does my parent give me?” - which is how component-based frameworks like React , Vue , and Web Components already expect components to work.
Container Query Syntax and Containment Types
Container queries require a two-step setup: declare a containment context on the parent, then write conditional styles on the children.
Step 1 - Declare a container:
.card-wrapper {
container-type: inline-size;
}This tells the browser to track the inline (horizontal in LTR languages) size of .card-wrapper and make it queryable. Without this declaration, @container rules targeting this element will silently do nothing.
Step 2 - Write container-conditional styles:
@container (min-width: 400px) {
.card {
grid-template-columns: 150px 1fr;
}
}
@container (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr;
font-size: 1.1rem;
}
}The container-type property accepts three values:
| Value | Tracks | Use Case |
|---|---|---|
inline-size | Width only | 95% of responsive layouts (width-driven) |
size | Width and height | Rare cases needing height-based queries |
normal | Nothing (default) | No containment |
Stick with inline-size unless you specifically need height queries. size enables full containment in both axes, which carries layout performance implications.
Named Containers
When layouts get complex with nested containers, names prevent ambiguity:
.sidebar {
container: sidebar / inline-size;
}
@container sidebar (min-width: 300px) {
.widget {
display: grid;
grid-template-columns: 1fr 1fr;
}
}The shorthand container: sidebar / inline-size combines container-name and container-type. Without a name, @container resolves to the nearest ancestor that has containment set - which is usually fine for simple layouts but can cause surprises in deeply nested structures.
Container Query Units
Container queries also introduce a set of relative units scoped to the container rather than the viewport:
cqw- 1% of container widthcqh- 1% of container heightcqi- 1% of container inline sizecqb- 1% of container block sizecqminandcqmax- the smaller/larger ofcqiandcqb
These work like vw/vh but relative to the component’s container. A practical use: font-size: 3cqi makes text scale proportionally to the container width, which is useful for hero cards or dashboard tiles where text should fill the available space.
Practical Patterns
Some UI patterns have always been painful to make responsive with media queries alone. The following three are where container queries pay off the most.
Responsive Card
At narrow container widths (under 300px), the card stacks vertically with the image on top and text below. At medium widths (300-500px), it switches to a horizontal layout. At wide widths (over 500px), it reveals additional metadata fields. All three layouts use identical HTML:
.card-grid {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
}
@container (min-width: 300px) {
.card {
grid-template-columns: 150px 1fr;
}
}
@container (min-width: 500px) {
.card {
grid-template-columns: 200px 1fr;
}
.card__meta {
display: block;
}
}Collapsing Navigation
A navigation component inside a container-type: inline-size parent switches from horizontal links to a vertical hamburger menu layout based on available space:
.nav-container {
container-type: inline-size;
}
.nav {
display: flex;
flex-direction: column;
}
@container (min-width: 600px) {
.nav {
flex-direction: row;
gap: 2rem;
}
.nav__toggle {
display: none;
}
}This works identically whether the nav sits in a full-width header or gets squeezed into a sidebar panel.
Adaptive Data Table
Wide tables show all columns. At medium container widths, less-important columns are hidden. At narrow widths, the table transforms into a stacked card layout using data-label attributes:
.table-wrapper {
container-type: inline-size;
}
@container (max-width: 600px) {
.table th.secondary,
.table td.secondary {
display: none;
}
}
@container (max-width: 400px) {
.table thead { display: none; }
.table tr {
display: block;
margin-bottom: 1rem;
border: 1px solid #ddd;
padding: 0.5rem;
}
.table td::before {
content: attr(data-label) ": ";
font-weight: bold;
}
}Style Queries and What Comes Next
Beyond size-based queries, CSS is gaining style queries that apply styles based on a parent’s custom property values. The syntax looks like this:
@container style(--theme: dark) {
.card {
background: #1a1a1a;
color: #e0e0e0;
}
}Style queries for custom properties currently work in Chrome 111+, Edge 111+, and Safari. Firefox support is expected to land sometime in 2026. The practical application is component-level theming without class toggles: set --density: compact on a container and let child components automatically adjust their padding and font sizes through pure CSS.
You can also combine size and style queries:
@container (min-width: 400px) and style(--variant: featured) {
.card {
grid-template-columns: 1fr 1fr;
border: 2px solid var(--accent);
}
}This applies styles only when the container is wide enough AND has the featured variant flag set - contextual component variants that previously required JavaScript.
Performance Notes
Setting container-type: inline-size enables size containment, meaning the browser knows that children cannot affect the parent’s inline size. This tends to be a performance win over unconstrained layouts because the browser can skip certain layout recalculations. The size value is more expensive since it requires containment in both axes - use it only when you need height-based queries.
Polyfill for Legacy Browsers
For the roughly 4% of browsers without native support, the container-query-polyfill
from Google Chrome Labs provides a 9KB (compressed) shim using ResizeObserver under the hood. The polyfill is in maintenance mode since late 2022 and supports the full @container syntax including container query units. Modern browsers ignore it and use native support. Note that the polyfill does not support Shadow DOM or calc() inside container conditions - edge cases that rarely matter in practice.
Given the current 96%+ support level, most production sites can use container queries without a polyfill and treat the remaining gap as a progressive enhancement concern. The components still render; they just stay in their default (usually stacked) layout on unsupported browsers.