CSS Container Queries: The Complete Responsive Design Guide for 2025
Picture this: you've built a stunning product card component that looks perfect at viewport widths above 1200px. The layout is clean, the spacing is balanced, and stakeholders approve it. But the moment a designer places that same component in a sidebar—a 300px-wide container—it falls apart. Text overlaps. Images squash awkwardly. The three-column grid you designed becomes a jumbled mess.
This is the fundamental problem that media queries, for all their power, cannot solve. Media queries are tied to the viewport size, not to the actual container holding your component. You'd need separate CSS for every possible layout context where that card appears—sidebar, footer, full-width section, card grid. That's not scalable. That's not component-driven design.
Enter CSS container queries: a paradigm shift for responsive design in 2025. Instead of asking "how wide is the viewport?", container queries ask "how much space does my parent container give me?" This small change unlocks component-level responsiveness, eliminates media query redundancy, and makes reusable design systems a reality. No JavaScript required. No class toggles. Pure CSS.
In this guide, we'll explore everything you need to master container queries—from foundational syntax to real-world patterns, Angular integration, design token theming, and a complete migration strategy from media queries.
Why Media Queries Alone Are Not Enough in 2025
Media queries revolutionized responsive web design in the 2010s. By conditioning styles on viewport width, we could design for multiple screen sizes: mobile at 375px, tablet at 768px, desktop at 1200px. But this approach assumes a fundamental truth that rarely holds in modern component-driven applications: that your component's available width matches the viewport width.
In 2025, web applications are built with reusable, composable components. A product card might appear in:
- A full-width hero section (1200px)
- A three-column grid (360px per card)
- A right-hand sidebar (280px)
- An off-canvas drawer (250px)
With media queries alone, your CSS doesn't know which context the card is in. It only knows the viewport is, say, 1920px wide. So it applies desktop styles—which fail when that viewport-based "desktop width" is constrained to a 280px sidebar.
Teams work around this with utility classes (.card--sidebar), or they duplicate components, or they accept that some layouts will look broken. None of these solutions scale.
Container queries flip the model: instead of the component caring about the viewport, the component cares about its immediate container. "Am I in a 300px-wide space or a 1200px-wide space?" The component asks its parent, not the browser window. This enables truly responsive, reusable components that work in any layout context.
In 2025, with 93%+ browser support, container queries are no longer a future feature—they're a present-day best practice for professional, maintainable design systems.
What Are CSS Container Queries?
Container queries are defined in two parts: first, you establish a containment context on a parent element; second, you query the size or style of that container from within descendant elements.
Establishing a Containment Context
The container-type property tells the browser: "I want to be a queryable container." Three values are available:
inline-size(most common): Query the container's width. Ideal for horizontal responsive layouts. This is what you'll use 95% of the time.size: Query both width and height. Useful for grid-based components or intrinsic aspect-ratio designs.normal: No containment (default). Used to explicitly disable container queries.
.card-container {
container-type: inline-size;
container-name: card-holder;
}
The container-name property is optional but recommended—it labels your container so you can target it explicitly in queries, avoiding accidental cascading to unintended ancestors.
The @container Rule Syntax
Once a containment context exists, you query it with @container:
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 1fr 2fr;
}
}
@container (max-width: 300px) {
.card {
display: flex;
flex-direction: column;
}
}
This syntax mirrors @media but targets the container, not the viewport. You can combine conditions:
@container (min-width: 500px) and (min-height: 600px) {
.card {
/* Styles apply only in containers 500px+ wide AND 600px+ tall */
}
}
Container Queries vs Media Queries: Side-by-Side
Let's compare the same card component built with media queries (old approach) and container queries (modern approach).
Media Query Approach:
.card {
display: flex;
flex-direction: column;
padding: 1rem;
}
@media (min-width: 768px) {
.card {
display: grid;
grid-template-columns: 120px 1fr;
padding: 1.5rem;
}
}
@media (min-width: 1200px) {
.card {
grid-template-columns: 150px 1fr;
gap: 2rem;
}
}
The media query approach works great on a full-width page. But place this card in a 300px sidebar? It still applies mobile styles at 1920px viewport width. Broken.
Container Query Approach:
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
display: flex;
flex-direction: column;
padding: 1rem;
}
@container card (min-width: 300px) {
.card {
display: grid;
grid-template-columns: 100px 1fr;
padding: 1.5rem;
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 150px 1fr;
gap: 2rem;
}
}
Now the card adapts based on its actual container width, not the viewport. 300px sidebar? It flows vertically. 600px grid? It goes horizontal. 1200px full-width? Maximum spacing. One component, infinite layout contexts, zero hacks.
| Aspect | Media Queries | Container Queries |
|---|---|---|
| Query Target | Viewport width | Container width |
| Use Case | Page-level layout | Component-level layout |
| Reusability | Limited across contexts | Works anywhere |
| Specificity | Global breakpoints | Local breakpoints |
| JavaScript Needed | No | No |
| Browser Support 2025 | 100% | 93%+ |
Core Patterns: Cards, Grids, Sidebars
Responsive Card Component
The product card is a canonical example. Here's a complete pattern: at small container widths, the image stacks above the text; at medium widths, they sit side-by-side; at large widths, the layout widens and adds additional details.
.product-card-wrapper {
container-type: inline-size;
container-name: product-card;
}
.product-card {
display: flex;
flex-direction: column;
background: var(--surface);
border-radius: var(--radius-md);
padding: var(--spacing-md);
}
.product-card-image {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
margin-bottom: var(--spacing-sm);
}
.product-card-title {
font-size: 1rem;
font-weight: 600;
line-height: 1.3;
}
/* At 280px+, place image and text side-by-side */
@container product-card (min-width: 280px) {
.product-card {
flex-direction: row;
align-items: flex-start;
}
.product-card-image {
width: 80px;
height: 80px;
margin-bottom: 0;
margin-right: var(--spacing-md);
flex-shrink: 0;
}
}
/* At 500px+, add price and rating */
@container product-card (min-width: 500px) {
.product-card {
padding: var(--spacing-lg);
}
.product-card-image {
width: 120px;
height: 120px;
}
.product-card-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-md);
}
}
Auto-Sizing Grid Without Breakpoints
Traditional grids use media queries to switch column counts. Container queries offer an elegant alternative: a grid that sizes itself proportionally based on available width.
.grid-wrapper {
container-type: inline-size;
container-name: grid;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
/* Tighter gap on smaller containers */
@container grid (max-width: 400px) {
.grid {
gap: var(--spacing-sm);
}
}
/* Larger cards on bigger containers */
@container grid (min-width: 800px) {
.grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
}
This grid automatically reflows based on its container, not the viewport. One grid definition works in a sidebar, a footer, or a full-width section.
Sidebar-Aware Navigation
Navigation menus commonly need different layouts in header vs. sidebar contexts. Container queries handle this elegantly:
.nav-container {
container-type: inline-size;
container-name: nav;
}
.nav {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.nav-item {
padding: var(--spacing-md) var(--spacing-sm);
border-radius: var(--radius-sm);
}
/* Horizontal layout at wider widths */
@container nav (min-width: 600px) {
.nav {
flex-direction: row;
gap: var(--spacing-lg);
}
.nav-item {
padding: var(--spacing-sm) var(--spacing-md);
}
}
CSS Style Queries: Theme-Aware Components
While size queries are the most common, CSS also supports style queries—a newer feature that lets you condition styles on a container's computed custom property values. This unlocks theme-aware components without class toggles or JavaScript.
@container style() Syntax
Style queries check if a parent container has set a specific CSS custom property:
.theme-container {
container-type: inline-size;
--theme-mode: light;
--theme-color: #3b82f6;
}
.card {
background: var(--theme-color, #e0e0e0);
color: var(--text-light, #333);
}
/* Apply dark colors when container sets --theme-mode: dark */
@container style(--theme-mode: dark) {
.card {
background: var(--theme-color-dark, #1e293b);
color: var(--text-dark, #f5f5f5);
}
}
Dark/Light Theme Toggling
Style queries enable theme switching at the container level, not globally:
.page-wrapper {
container-type: inline-size;
--theme-mode: light;
}
/* Applying dark theme sets the custom property */
.page-wrapper[data-theme="dark"] {
--theme-mode: dark;
}
.surface {
background: var(--surface-light, #fafafa);
color: var(--text-light, #1a1a1a);
}
@container style(--theme-mode: dark) {
.surface {
background: var(--surface-dark, #0f172a);
color: var(--text-dark, #f8f8f8);
}
}
Design Token–Driven Theming
Combine style queries with design tokens for maximum flexibility:
:root {
/* Define tokens for light mode */
--color-surface: #fafafa;
--color-text: #1a1a1a;
--color-border: #e5e5e5;
}
[data-theme="dark"] {
--color-surface: #0f172a;
--color-text: #f8f8f8;
--color-border: #334155;
}
.card-container {
container-type: inline-size;
}
.card {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
Every component automatically adapts to the theme because they use the same custom property names. Style queries can also validate tokens for advanced scenarios.
Container Queries in Angular
Angular's component model aligns perfectly with container queries. Each component can define its own containment context, enabling modular, responsive designs without global breakpoint management.
ViewEncapsulation and Containment Context
Angular's ViewEncapsulation modes (Emulated, ShadowDom, None) work seamlessly with container queries. Even with Emulated encapsulation (the default), container queries function normally because containment is a CSS concept, not DOM-dependent.
Standalone Component Example
Here's a reusable card component with container-type defined in its host styles:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="card">
<img [src]="product.image" [alt]="product.name" class="card-image" />
<div class="card-content">
<h3 class="card-title">{{ product.name }}</h3>
<p class="card-description">{{ product.description }}</p>
<div class="card-footer">
<span class="card-price">${{ product.price }}</span>
<button class="card-button">Add to Cart</button>
</div>
</div>
</div>
`,
styles: [`
:host {
container-type: inline-size;
container-name: product-card;
display: block;
}
.card {
display: flex;
flex-direction: column;
padding: 1rem;
background: var(--surface, white);
border-radius: 0.5rem;
}
.card-image {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
margin-bottom: 1rem;
}
.card-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
}
.card-description {
font-size: 0.875rem;
color: var(--text-secondary, #666);
margin: 0 0 1rem 0;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Horizontal layout at 300px+ */
@container product-card (min-width: 300px) {
.card {
flex-direction: row;
padding: 1.5rem;
}
.card-image {
width: 100px;
height: 100px;
margin-bottom: 0;
margin-right: 1.5rem;
flex-shrink: 0;
}
}
/* Expanded layout at 600px+ */
@container product-card (min-width: 600px) {
.card {
padding: 2rem;
}
.card-image {
width: 150px;
height: 150px;
}
.card-title {
font-size: 1.25rem;
}
}
`],
})
export class ProductCardComponent {
@Input() product = {
name: 'Sample Product',
description: 'High-quality product description',
price: 99.99,
image: '/assets/placeholder.jpg',
};
}
OnPush Change Detection + Container Queries
Container queries and OnPush change detection interact cleanly. Container queries don't require Angular to know about size changes—CSS handles it. This means OnPush strategy works without caveats:
@Component({
selector: 'app-responsive-grid',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [`
:host {
container-type: inline-size;
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
@container (min-width: 800px) {
.grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
}
`],
})
export class ResponsiveGridComponent {
items = signal(['Item 1', 'Item 2', 'Item 3']);
}
The grid reflows automatically based on its container width, and Angular's OnPush doesn't interfere because no state changes are required—it's pure CSS.
Design System Integration
Container queries are a natural fit for design systems that use design tokens and component hierarchies.
Tokens + Container Queries: A Powerful Pair
Combine CSS custom properties (tokens) with container queries for data-driven responsive design:
:root {
/* Spacing tokens */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Breakpoint tokens */
--container-sm: 320px;
--container-md: 600px;
--container-lg: 1000px;
/* Typography tokens */
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.25rem;
}
.card-wrapper {
container-type: inline-size;
}
.card {
padding: var(--spacing-md);
}
@container (min-width: var(--container-md)) {
.card {
padding: var(--spacing-lg);
font-size: var(--font-size-lg);
}
}
This approach centralizes design decisions. Update a token, and all responsive behaviors update automatically. Designers and developers speak the same language—the language of your design system.
Storybook-Driven Development
Storybook works beautifully with container queries. Create a "Container Query" decorator that renders components in various container widths:
// story.ts
export default {
title: 'Components/Card',
component: CardComponent,
decorators: [
(story) => ({
template: `
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<div style="width: 280px; border: 1px dashed #ccc; padding: 1rem;">
<h4>280px (Sidebar)</h4>
<ng-container *ngTemplateOutlet="story"></ng-container>
</div>
<div style="width: 600px; border: 1px dashed #ccc; padding: 1rem;">
<h4>600px (Main)</h4>
<ng-container *ngTemplateOutlet="story"></ng-container>
</div>
<div style="width: 1200px; border: 1px dashed #ccc; padding: 1rem;">
<h4>1200px (Full)</h4>
<ng-container *ngTemplateOutlet="story"></ng-container>
</div>
</div>
`,
}),
],
};
export const Default: StoryObj = {
args: {
product: { name: 'Product', price: 99 },
},
};
This visually demonstrates how your component adapts across container widths. Stakeholders see the component working in real contexts, not just ideal desktop widths.
Container Units: cqw, cqh, cqi, cqb, cqmin, cqmax
Container queries introduce six new CSS units for sizing elements relative to their container:
| Unit | Meaning | Example |
|---|---|---|
cqw |
1% of container width | width: 50cqw = half container width |
cqh |
1% of container height | height: 50cqh = half container height |
cqi |
1% of container inline size | Same as cqw (LTR/horizontal) |
cqb |
1% of container block size | Same as cqh (vertical) |
cqmin |
Smaller of width or height | font-size: 5cqmin = responsive to smaller dimension |
cqmax |
Larger of width or height | padding: 2cqmax = scales with larger dimension |
Fluid Sizing with Container Units
Replace clamp() with container units for fluid typography and spacing:
.card-wrapper {
container-type: inline-size;
}
.card-title {
/* Instead of clamp(1rem, 5vw, 2rem), use container units */
font-size: clamp(1rem, 5cqw, 2rem);
line-height: 1.3;
}
.card-padding {
/* Padding scales with container width */
padding: 3cqw;
}
.card {
/* Use cqmin to scale with the smaller dimension (width or height) */
gap: 1cqmin;
}
Container units are particularly powerful in design systems where tokens combine with container dimensions for truly responsive spacing.
Performance Considerations
Container queries are CSS-native, which means excellent performance compared to JavaScript-based alternatives. However, understanding the implications helps you use them optimally.
Container Queries and Layout Thrashing
Container queries don't cause layout thrashing because they're evaluated during the normal CSS rendering pipeline. Unlike JavaScript resize listeners that measure elements and apply styles reactively, container queries are part of CSS's calculation phase.
No JavaScript Overhead
Container queries require zero JavaScript. Compare to traditional approaches:
- JavaScript ResizeObserver: Listener fires on every resize → recalculation → style updates → layout thrash
- CSS Media Queries: Global, viewport-only, no component awareness
- CSS Container Queries: Browser-native containment, evaluated efficiently during style recalculation
Containment Benefits for Rendering
The container-type property activates CSS containment, which has broad performance benefits:
- Reduces layout calculations for descendants
- Enables independent paint layers
- Allows the browser to optimize reflow within contained subtrees
For large applications with hundreds of components, containment can meaningfully reduce render time.
Migration Guide: From Media Queries to Container Queries
Migrating an existing design system from media queries to container queries is straightforward and can be done incrementally.
Step 1: Audit Your Media Queries
Identify which breakpoints are component-level vs. page-level. Component-level breakpoints are candidates for container queries; page-level breakpoints (like full-page layout shifts) should remain as media queries.
/* Component-level: migrate to @container */
@media (min-width: 768px) {
.card { /* ... */ }
}
/* Page-level: keep as @media */
@media (min-width: 1200px) {
.container { max-width: 1200px; }
}
Step 2: Add Containment to Parent Elements
Wrap your component with a container element or apply container-type directly:
.card-wrapper {
container-type: inline-size;
container-name: card;
}
Step 3: Convert @media to @container
Replace media query breakpoints with container query equivalents:
/* Before */
@media (min-width: 768px) {
.card { display: grid; }
}
/* After */
@container card (min-width: 768px) {
.card { display: grid; }
}
Step 4: Test Across Contexts
Verify your component works in multiple layout contexts:
- Sidebar (280px)
- Main column (600px)
- Full-width (1200px)
- Off-canvas drawer (250px)
What to Keep as Media Queries
Page-level layout decisions should remain as @media queries:
/* Page wrapper breakpoints */
@media (min-width: 1200px) {
body {
display: grid;
grid-template-columns: 250px 1fr 300px; /* Sidebar, main, aside */
}
}
/* Component breakpoints */
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { /* Component-specific layout */ }
}
This hybrid approach gives you the best of both worlds: page-level control via media queries and component-level flexibility via container queries.
Browser Support and Progressive Enhancement
As of 2025, container queries have excellent browser support:
- Chrome/Edge: 105+ (March 2023)
- Firefox: 110+ (February 2023)
- Safari: 16+ (September 2022)
- Mobile browsers: Uniformly supported across iOS Safari, Chrome Mobile, Samsung Browser
Global coverage exceeds 93%, making container queries safe for production use without progressive enhancement layers.
For older browsers, container queries simply don't apply—styles fall back to mobile-first defaults. This is acceptable degradation: older browser users get a functional (if less optimized) experience.
Closing: Design for the Container, Not the Viewport
CSS container queries represent a fundamental shift in how we think about responsive design. For decades, we've designed for viewports—imaginary screen sizes that drive layout decisions. Container queries invert this model: we design for containers, for the actual space available to our components.
This shift unlocks a new level of reusability. A card designed with container queries works anywhere: sidebars, grids, drawers, modals, full-width sections. No class overrides. No media query duplication. One component, infinite contexts.
In 2025, with near-universal browser support, container queries are no longer aspirational. They're the foundation of modern, maintainable design systems. If you're still relying solely on media queries for component responsiveness, now is the time to migrate.
Start small: pick one component, add container-type: inline-size to its parent, convert your media queries to @container rules, and observe how it adapts across your application. You'll quickly recognize the power—and won't look back.
Further Reading & Related Topics:
- For advanced fluid typography without container units, see our guide on
/blogs/fluid-typography-css-clamp-responsive-type-scale-2025 - Design token strategies are explored in depth at
/blogs/atomic-design-vs-component-driven-design-patterns-2025
