The Fundamental Problem Media Queries Couldn't Solve
Media queries changed web development when they arrived. For the first time, designers could specify different layouts for different screen sizes without JavaScript. But media queries have a fundamental limitation: they can only see the viewport.
Consider a card component. In a 3-column grid on a 1200px screen, each card is roughly 380px wide. In a 1-column layout on a 768px tablet, the same card is 720px wide. A media query cannot distinguish between these two states because the viewport is the same. The card's context -- its parent container -- is invisible to the CSS.
/* Media query cannot solve this */
.card {
flex-direction: column; /* correct for 380px */
/* but wrong for 720px */
}
Developers have hacked around this for years. JavaScript-based container queries. CSS grids with auto-fill and minmax. Arbitrary breakpoints duplicated across components. Each workaround adds complexity and maintenance burden. None of them truly solves the problem.
Container queries are the CSS-native solution. They let elements respond to the size of their parent container, not the viewport. This is not an incremental improvement -- it is a fundamental shift in how we think about responsive design.
How Container Queries Work
Container queries introduce two new CSS concepts: containment and querying.
Establishing a Container
Use container-type to establish a containment context:
.card-grid {
container-type: inline-size;
container-name: card-grid;
}
The container-type property accepts three values:
| Value | Behavior |
|---|---|
inline-size |
Queries can check the inline axis (width in horizontal writing mode) |
size |
Queries can check both axes (triggers containment on both) |
normal |
No containment, but can still participate as a named container |
Use inline-size by default. The size value imposes stricter containment that can interfere with layouts, and normal is primarily useful for named container hierarchies.
The container-name is optional but recommended. Without it, the nearest container ancestor with a container-type is used. With a name, you can target specific containers in a nested hierarchy.
Writing Container Queries
The @container at-rule mirrors @media syntax but queries the container instead of the viewport:
@container card-grid (min-width: 600px) {
.card {
flex-direction: row;
}
}
@container card-grid (min-width: 400px) and (max-width: 599px) {
.card {
flex-direction: row;
flex-wrap: wrap;
}
}
@container card-grid (max-width: 399px) {
.card {
flex-direction: column;
}
}
Container queries support the same comparison operators as media queries: min-width, max-width, min-height, max-height, and the range syntax (400px <= width <= 600px).
Container Query Length Units
Container queries also introduce new units that reference the container's size:
| Unit | Relative To |
|---|---|
cqw |
1% of container width |
cqh |
1% of container height |
cqi |
1% of container inline size |
cqb |
1% of container block size |
cqmin |
The smaller of cqi and cqb |
cqmax |
The larger of cqi and cqb |
These are invaluable for typography and spacing that scale with the container:
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
.card-body {
font-size: clamp(0.875rem, 2.5cqi, 1rem);
padding: 2cqi;
}
Unlike viewport units (vw, vh), container units are composable. A component that uses cqi for its spacing will scale correctly whether it is rendered in a sidebar, a main content area, or a card within a grid.
Container Queries vs. Media Queries
| Concern | Media Queries | Container Queries |
|---|---|---|
| Query target | Viewport dimensions | Container dimensions |
| Component reuse | Requires knowing viewport context | Context-agnostic |
| Testability | Resize browser or iframe | Resize container element |
| Nested layouts | Impossible (one viewport) | Fully supported |
| Unit reference | Viewport units (vw, vh) | Container units (cqw, cqh) |
| IE compatibility | IE 9+ | Not supported (modern only) |
| Mental model | Page-level layout | Component-level layout |
The two are not mutually exclusive. Use media queries for page-level layout -- how many columns your grid has, whether the sidebar is visible, how much padding the main content area has. Use container queries for component-level responsiveness -- how a component looks within its allocated space.
Real-World Use Cases
Dashboards
Dashboards are the killer use case for container queries. A dashboard widget needs to render correctly whether it occupies a quarter of the screen or the full viewport. Users resize panels, rearrange layouts, and compare data across configurations. Media queries cannot handle this.
/* Dashboard widget -- responsive to its panel, not the viewport */
.widget-container {
container-type: inline-size;
container-name: widget;
}
@container widget (min-width: 500px) {
.widget-header {
flex-direction: row;
align-items: center;
}
.widget-chart {
display: block;
}
.widget-legend {
display: flex;
gap: 1rem;
}
}
@container widget (max-width: 499px) {
.widget-header {
flex-direction: column;
}
.widget-chart {
display: none;
}
.widget-legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
}
In a drag-and-drop dashboard, the same widget component renders correctly at any size. No JavaScript resize observers. No media query breakpoint math. Pure CSS.
Design Systems
Design system components are the second major use case. A button, card, or input must work in the sidebar (narrow), the main content area (wide), and inside a modal (medium). Without container queries, each component needs either a JavaScript-resize-based approach or multiple variants.
/* Design system: DataTable -- works in any context */
.data-table-container {
container-type: inline-size;
container-name: data-table;
}
@container data-table (max-width: 400px) {
.data-table thead {
display: none;
}
.data-table tr {
display: grid;
border: 1px solid var(--color-border);
margin-bottom: 1rem;
}
.data-table td {
display: flex;
justify-content: space-between;
}
.data-table td::before {
content: attr(data-label);
font-weight: 600;
}
}
This pattern makes design systems dramatically simpler. Instead of maintaining DataTable, DataTableCompact, and DataTableResponsive variants, you maintain one component with container query breakpoints.
Consider a more complex design system example -- a navigation component that needs to adapt based on its container:
.nav-container {
container-type: inline-size;
container-name: nav;
}
@container nav (min-width: 600px) {
.nav-links {
display: flex;
gap: 1rem;
}
.nav-toggle {
display: none;
}
}
@container nav (max-width: 599px) {
.nav-links {
display: none;
}
.nav-toggle {
display: block;
}
}
@container nav (max-width: 599px) and (min-height: 200px) {
.nav-links.expanded {
display: flex;
flex-direction: column;
}
}
This navigation renders as a horizontal bar in wide containers, a hamburger menu in narrow ones, and expands vertically when triggered. It works identically in the page header, inside a sidebar, or within a modal.
Email Templates
Email template rendering is an unexpected but powerful use case. Email clients render content in narrow preview panes, wide reading panes, and everything in between. Container queries let email developers build responsive templates without relying on clunky @media only queries.
.email-container {
container-type: inline-size;
container-name: email;
}
@container email (min-width: 480px) {
.email-content {
padding: 2rem;
}
.email-image {
width: 50%;
float: left;
margin-right: 1rem;
}
}
@container email (max-width: 479px) {
.email-content {
padding: 1rem;
}
.email-image {
width: 100%;
}
}
Note that email support for container queries is still limited to web-based email clients. For full email client coverage, continue using @media queries alongside @container as a progressive enhancement.
Browser Support and Polyfill Strategy
Container queries are supported in all major browsers:
| Browser | Minimum Version | Release Date | Status |
|---|---|---|---|
| Chrome | 105 | Aug 2022 | Supported |
| Edge | 105 | Aug 2022 | Supported |
| Firefox | 110 | Feb 2023 | Supported |
| Safari | 16.4 | Mar 2023 | Supported |
| Opera | 91 | Sep 2022 | Supported |
| Samsung Internet | 20 | 2023 | Supported |
| IE 11 | N/A | N/A | Not supported |
As of early 2026, container queries have been stable in all evergreen browsers for over two years. The only environments that lack support are legacy browsers (IE 11, older Safari versions). If your analytics show less than 2% traffic on unsupported browsers, you can use container queries without a polyfill.
Polyfill Strategy
If you need to support older browsers, the container-query-polyfill by Google Chrome Labs is the established solution:
npm install container-query-polyfill
// Import in your application entry point
import 'container-query-polyfill';
The polyfill works by observing DOM mutations and resize events, measuring containers, and applying the appropriate styles via ResizeObserver. It has some limitations:
- It relies on
ResizeObserver, which itself is not supported in IE 11 - Performance impact increases with the number of container elements under observation
- Style queries (
@container style(...)) are not polyfilled - Container query length units (
cqw,cqh, etc.) require a separate postCSS plugin or manual fallback
Progressive Enhancement Approach
For most production applications in 2026, the polyfill is unnecessary. Use a progressive enhancement approach instead:
/* Fallback: the default layout works in all browsers */
.card {
flex-direction: column;
}
/* Enhancement: container query for wider containers */
@supports (container-type: inline-size) {
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
}
This approach ensures a functional layout in all browsers while providing the enhanced experience in modern ones. The @supports rule is widely supported and keeps your CSS clean and maintainable.
Migration Guide: Moving From @media to @container
Migrating an existing codebase to container queries does not require a rewrite. Follow this incremental approach.
Step 1: Audit Component-Level Media Queries
Search for @media rules in your component files. These are prime candidates for container query migration:
grep -r "@media" src/components/ --include="*.css" --include="*.module.css"
Categorize each one: does it control page-level layout or component-level appearance? Component-level rules are the ones to migrate.
Step 2: Add Containment to Parent Elements
Identify the direct parents of responsive components and add container-type:
/* Before: media query in the component */
.product-card {
/* base styles */
}
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* After: containment on the parent */
.product-grid {
container-type: inline-size;
}
.product-card {
/* base styles */
}
@container (min-width: 400px) {
.product-card {
flex-direction: row;
}
}
Note the changed breakpoint. Media query breakpoints are viewport-relative and typically larger. Container query breakpoints should be based on the component's actual rendered width.
Step 3: Converge Breakpoints
Media query breakpoints are viewport-relative (768px, 1024px). Container query breakpoints should be component-relative. A card that needs to switch layout at around 400px of container width might need 350px or 450px depending on padding and gaps. Adjust breakpoints based on the component's actual rendered width, not the viewport.
This is the most important step. Do not simply replace @media with @container and keep the same breakpoint values. Re-evaluate what width the component actually sees in each context.
Step 4: Test Across Contexts
This is where container queries shine. Test the component in a narrow sidebar, a wide main area, and a medium-sized card. If the breakpoints are well-chosen, the component looks good in all contexts without any additional code.
/* Test harness for container query breakpoints */
.test-grid {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 1rem;
}
.test-grid .component-wrapper {
container-type: inline-size;
}
Step 5: Remove Old Media Queries
Once a component is migrated and tested in all contexts, remove the old @media rules. This reduces CSS size and eliminates the possibility of conflicts between old and new responsive logic.
- @media (min-width: 768px) {
- .product-card {
- flex-direction: row;
- }
- }
+ @container (min-width: 400px) {
+ .product-card {
+ flex-direction: row;
+ }
+ }
Advanced Patterns
Nested Containers
Container queries nest naturally. A page contains panels, panels contain widgets, widgets contain cards. Each level queries its direct parent container.
.page {
container-name: page;
container-type: inline-size;
}
.panel {
container-name: panel;
container-type: inline-size;
}
@container page (min-width: 1000px) {
.panel {
grid-template-columns: 1fr 1fr;
}
}
@container panel (min-width: 400px) {
.widget {
flex-direction: row;
}
}
The container-name property is essential in nested scenarios. Without it, @container queries the nearest ancestor with container-type, which may not be the one you intended. By naming your containers, you can be explicit about which level you are querying.
Style Queries
Style queries are a newer addition to the spec. They let you query computed style values, not just dimensions:
@container style(--theme: dark) {
.card {
background: var(--color-surface-dark);
color: var(--color-text-dark);
}
}
@container style(--layout: compact) {
.card {
padding: 0.5rem;
font-size: 0.875rem;
}
}
Style queries are less widely supported than size queries. Use them with progressive enhancement or when targeting modern browsers exclusively. They are particularly useful in design systems where a parent component sets CSS custom properties that child components should respond to.
Container Query Units for Typography
Container query units solve a long-standing responsive typography problem: font sizes that scale with the component, not the viewport.
.card-content {
font-size: clamp(0.875rem, 3cqi, 1.25rem);
line-height: 1.6;
padding: 3cqi;
}
In a 300px-wide container, the font is 0.875rem (14px). In an 800px-wide container, it is 1.25rem (20px). The typography scales smoothly across the full range without any breakpoint-based jumps.
This pattern works especially well for data visualization components where text labels need to scale with the chart area:
.chart-legend {
font-size: clamp(0.75rem, 2cqi, 1rem);
}
.chart-value {
font-size: clamp(1.25rem, 5cqi, 3rem);
}
Container Queries in CSS Modules
Container queries work seamlessly with CSS Modules in Next.js and other frameworks:
/* Card.module.css */
.grid {
container-type: inline-size;
container-name: card-grid;
}
@container card-grid (min-width: 400px) {
.card {
flex-direction: row;
}
}
There is no special configuration needed. CSS Modules handle the scoping correctly, and the @container at-rule works within the module scope.
Performance Considerations
Container queries do not introduce meaningful performance overhead. The browser's layout engine already tracks element sizes. Container queries simply expose that information to CSS.
However, there are a few patterns to avoid:
- Excessive container nesting -- deeply nested containers (5+ levels) can increase style recalculation time. Keep nesting to 2-3 levels in practice.
- Animating container sizes -- if a container's size is animated, container queries re-evaluate on each frame. Use
will-change: container-typeor avoid animating container dimensions. - Too many containers on one page -- hundreds of container elements on a single page can slow down initial style calculation. Use
container-nameto limit query scope.
In practice, container queries perform well at real-world scales. The WEIRDSOFT team uses them in production dashboards with 50+ container elements per page without any measurable performance impact.
Container queries are not the future of responsive design -- they are the present. Every modern browser supports them. The patterns are well-established. The migration is incremental. If you are still writing component styles that check the viewport width, you are working harder than you need to.
Responsive components, not responsive pages.