Introduction
Responsive design has been the cornerstone of modern web development since Ethan Marcotte coined the term in 2010. Media queries gave us the ability to adapt layouts to different screen sizes, but they've always had a fundamental limitation: they respond to the viewport, not the component's actual context. A sidebar widget, a card in a grid, and a main content block all receive the same media query breakpoints, even though they have vastly different amounts of available space.
CSS Container Queries represent the most significant advancement in responsive design since media queries themselves. They allow components to adapt their layout based on the size of their containing element, making them truly self-contained and reusable across any layout context. This is the future of component-driven design systems.
This guide explores container queries from the ground up — the problems they solve, the syntax you need to know, and the practical patterns that will transform how you build responsive components.
Understanding Container Queries: Core Concepts
The Problem with Media Queries
Consider a card component used in three contexts: a full-width main area, a two-column sidebar layout, and a narrow footer. With media queries, you'd need to know the exact viewport width at which each context changes and write breakpoints accordingly. If a designer moves the card from the sidebar to the main area, the media query breakpoints become wrong and need manual adjustment.
Container queries eliminate this problem entirely. The card simply responds to how much space it actually has, regardless of where it lives in the page hierarchy.
How Container Queries Work
The API has two parts. First, you mark an element as a query container using container-type. This tells the browser to track that element's dimensions. Then you write @container rules that conditionally apply styles based on the container's measured size.
/* Declare the container */
.card-parent {
container-type: inline-size;
container-name: card-parent;
}
/* Query the container */
@container card-parent (min-width: 400px) {
.card {
flex-direction: row;
gap: 24px;
}
}The Containment Model
When you set container-type: inline-size, the browser applies CSS containment (contain: inline-size on the element). This tells the browser that the element's inline size is determined by its parent, not its children. This is essential for performance — without containment, the browser would need to lay out children to determine the container's size, creating a circular dependency.
Container Query Length Units
Container queries introduce units that are relative to query containers:
@container card (inline-size > 0) {
.card-title {
/* Scale font size relative to container width */
font-size: clamp(1rem, 3cqi, 2rem);
}
.card-body {
/* Padding scales with container */
padding: clamp(12px, 3cqi, 32px);
}
}These units make it possible to create fluid designs that scale smoothly within their containers, not just relative to the viewport.
Architecture and Design Patterns
The Self-Contained Component Pattern
The primary architectural pattern is the self-contained responsive component. The component defines its own breakpoints based on its content needs, not the page's layout. A card might have three internal layouts: compact (under 300px), standard (300-500px), and expanded (over 500px). These breakpoints are intrinsic to the component and don't change when the component moves between page contexts.
The Design Token Integration Pattern
Container queries integrate naturally with design systems. Define container breakpoints as design tokens and reference them consistently across components. This creates a coherent responsive behavior where all components in the same container context adapt at the same thresholds.
The Progressive Enhancement Pattern
Use container queries as a progressive enhancement over media queries. Base styles and narrow layouts use media queries for broad page structure, while container queries fine-tune component layouts within that structure. This provides a fallback for browsers that don't support container queries while taking advantage of the feature where available.
The Layout-Driven Component Pattern
The page layout determines how many columns the grid has (using media queries), and each grid cell creates a container query context. Components within cells then adapt to their actual rendered width. This separation of concerns — layout responsibility at the page level, content adaptation at the component level — is the architectural sweet spot for container queries.
Step-by-Step Implementation
Setting Up a Responsive Card
Start with a card that needs to adapt to its available width:
<article class="card-wrapper">
<div class="card">
<figure class="card-media">
<img src="photo.jpg" alt="Description">
</figure>
<div class="card-body">
<h3 class="card-title">Responsive Card Title</h3>
<p class="card-description">
This card adapts its layout based on available container width,
not the viewport size.
</p>
<div class="card-actions">
<button class="btn btn-primary">Read More</button>
<button class="btn btn-ghost">Share</button>
</div>
</div>
</div>
</article>/* Container declaration */
.card-wrapper {
container: card / inline-size;
}
/* Base layout: stacked */
.card {
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.card-media img {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-body {
padding: 16px;
}
.card-actions {
display: none;
}
/* Medium layout: horizontal with image */
@container card (min-width: 380px) {
.card {
flex-direction: row;
align-items: stretch;
}
.card-media {
width: 180px;
flex-shrink: 0;
}
.card-media img {
height: 100%;
}
.card-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
}
/* Large layout: expanded with more detail */
@container card (min-width: 560px) {
.card-media {
width: 240px;
}
.card-body {
padding: 24px;
}
.card-title {
font-size: 1.5rem;
margin-bottom: 8px;
}
.card-description {
font-size: 1rem;
line-height: 1.7;
}
}Building a Responsive Sidebar Navigation
.nav-container {
container: nav / inline-size;
}
.nav {
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
}
.nav-label {
display: block;
}
/* Narrow: icon-only mode */
@container nav (max-width: 80px) {
.nav {
align-items: center;
}
.nav-label {
display: none;
}
.nav-item {
justify-content: center;
padding: 10px;
}
}Using Container Units for Fluid Spacing
.gallery-container {
container: gallery / inline-size;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: clamp(8px, 2cqi, 24px);
padding: clamp(8px, 3cqi, 32px);
}
.gallery-item img {
border-radius: clamp(4px, 1cqi, 12px);
}Real-World Use Cases
Use Case 1: Adaptive Data Tables
Data tables in dashboards often need to switch between a full table view and a card/list view based on available width. Container queries enable this without knowing the dashboard's column layout. Wide containers show the traditional table with all columns; narrow containers switch to a stacked card layout showing key fields.
Use Case 2: Comment Threads
Comment components in a forum or social platform need different layouts at different widths. In the main content area, comments show avatars, full text, and action buttons in a horizontal layout. In a narrow sidebar widget, the same component stacks vertically and hides less important metadata.
Use Case 3: Widget Configuration Panels
Configuration panels for dashboard widgets need to adapt to the widget's size. A small widget shows a compact settings form with stacked inputs. A large widget can display the form with side-by-side fields, additional options, and a live preview.
Use Case 4: Email Template Builder
In a drag-and-drop email builder, content blocks need to preview correctly at different widths. Container queries allow each block to show its mobile, tablet, and desktop layout independently, based on the preview pane's width rather than the editor's viewport.
Best Practices for Production
-
Define component-level breakpoints, not page-level ones — Choose breakpoints based on what makes sense for the component's content (e.g., where the text starts looking cramped), not based on standard device widths.
-
Use
container-type: inline-sizeexclusively for width-based queries — Only usecontainer-type: sizewhen you genuinely need height-based queries. Size containment is more expensive because it also affects the element's block sizing. -
Combine container queries with CSS Grid
auto-fill— Let the grid handle column count based on available space, and let container queries handle component layout within each cell. -
Use container units for proportional spacing — Replace viewport-based fluid spacing with
cqiunits so spacing scales correctly in all layout contexts. -
Provide fallback styles for non-supporting browsers — Use
@supportsto apply container query styles only where supported, with sensible defaults for older browsers.
/* Default styles for all browsers */
.card {
flex-direction: column;
}
/* Enhanced layout for supporting browsers */
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { flex-direction: row; }
}
}-
Avoid circular dependencies — A container cannot query itself. Don't make an element both a container and a target of its own container query.
-
Name all containers in multi-container layouts — When multiple containers are nested, unnamed container queries match the nearest ancestor container. Naming prevents ambiguity.
-
Test with dynamic content — Container sizes can change when content loads, images render, or text is truncated. Test container queries with realistic content that varies in length.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
Not setting container-type | @container rules never apply | Always declare container-type: inline-size on the parent |
Using width instead of inline-size | Fails in vertical writing modes | Use logical properties: inline-size, block-size |
| Container has intrinsic sizing | Circular dependency, broken layout | Container queries require explicit sizing from the parent |
| Too many nested containers | Performance overhead from containment | Minimize container declarations; share containers where possible |
Forgetting container-name in nested layouts | Query matches wrong container | Always name containers when multiple exist |
Expecting min-height queries without size type | Height queries silently fail | Use container-type: size for height queries |
Performance Optimization
Container queries use CSS containment, which is a performance feature. By telling the browser that an element's inline size is determined externally, the browser can skip recalculating that element's children when unrelated parts of the page change. This is particularly beneficial in large, complex layouts.
For optimal performance, apply container-type only to elements where you actually write @container rules. Every container declaration creates a containment boundary that the browser must track. In a page with 50 cards, if only the card wrapper needs container queries, declare containment there — not on every ancestor element.
Combine with content-visibility: auto for off-screen containers to skip rendering entirely:
.card-wrapper {
container: card / inline-size;
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}Comparison with Alternatives
| Feature | CSS Container Queries | Media Queries | ResizeObserver | matchMedia |
|---|---|---|---|---|
| Responds to | Container dimensions | Viewport dimensions | Element dimensions | Viewport dimensions |
| CSS-native | Yes | Yes | No (JS) | No (JS) |
| Performance | Excellent (containment) | Excellent | Good (debounced) | Good |
| Component-level | Yes | No | Yes | No |
| Browser support | All modern (2023+) | All modern | All modern | All modern |
| Cascade integration | Full | Full | Manual | Manual |
| Animatable | No | No | N/A | N/A |
Advanced Patterns
Combining with Subgrid
Container queries work alongside CSS Subgrid to create deeply responsive layouts:
.page-layout {
display: grid;
grid-template-columns: 250px 1fr;
}
.main-content {
container: main / inline-size;
}
@container main (min-width: 800px) {
.content-grid {
display: grid;
grid-template-columns: subgrid;
}
}Container Queries with Custom Properties
Use custom properties as container query triggers for theming:
.card-wrapper {
--density: compact;
container: card / inline-size;
}
@container card (min-width: 400px) {
.card-wrapper {
--density: comfortable;
}
}
@container card (min-width: 600px) {
.card-wrapper {
--density: spacious;
}
}
/* Density-based spacing */
.card-body {
padding: calc(var(--spacing-unit, 8px) *
(1 + (var(--density) == comfortable) + (var(--density) == spacious)));
}Media Query + Container Query Coordination
Use media queries for page-level layout and container queries for component-level adaptation in a coordinated strategy:
/* Page level: media query controls grid columns */
@media (min-width: 768px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.grid { grid-template-columns: repeat(3, 1fr); }
}
/* Component level: container query controls card layout */
.grid-item {
container: card / inline-size;
}
@container card (min-width: 350px) {
.card { flex-direction: row; }
}Testing Strategies
Container queries require testing at both viewport and container levels:
import { test, expect } from '@playwright/test';
test.describe('Container Queries', () => {
test('card adapts to narrow container', async ({ page }) => {
await page.goto('/demo/card');
const container = page.locator('.card-wrapper');
await container.evaluate(el => {
el.style.width = '250px';
});
const card = page.locator('.card');
await expect(card).toHaveCSS('flex-direction', 'column');
});
test('card adapts to wide container', async ({ page }) => {
await page.goto('/demo/card');
const container = page.locator('.card-wrapper');
await container.evaluate(el => {
el.style.width = '500px';
});
const card = page.locator('.card');
await expect(card).toHaveCSS('flex-direction', 'row');
});
test('card works in sidebar context', async ({ page }) => {
await page.goto('/demo/sidebar');
const sidebarCard = page.locator('.sidebar .card');
// Sidebar is narrow, card should stack
await expect(sidebarCard).toHaveCSS('flex-direction', 'column');
});
});Future Outlook
Container queries are part of a broader CSS evolution toward component-centric development. Style queries (querying computed values of custom properties) are in active development and will enable theming patterns without JavaScript. The @scope rule provides style encapsulation that pairs naturally with container queries — scoped, self-responsive components. Combined with the Popover API, View Transitions, and Anchor Positioning, CSS is becoming a complete component runtime.
Container Queries Performance
Container queries use ResizeObserver internally to detect size changes in container elements. This means container queries trigger layout recalculations when the container resizes, similar to how media queries trigger recalculations when the viewport resizes. For most applications, this overhead is negligible. However, avoid using container queries on elements that resize frequently during animations or scrolling, as this can cause excessive layout recalculations. Use the container-type: inline-size property instead of size when you only need width-based queries, as it reduces the number of properties the browser needs to track.
Container Query Debugging
Debug container queries using browser DevTools that highlight container boundaries and display the applied container context. Chrome DevTools shows a container badge on elements with container-type set, and clicking it highlights the container and its query-dependent children. Use the Elements panel to inspect computed container dimensions and verify that container-type is correctly applied. If container queries aren't working, check that the container element is an ancestor of the queried element and that the container has explicit dimensions set through CSS.
Conclusion
CSS Container Queries complete the responsive design story. The key takeaways are:
- Components should respond to their container, not the viewport — This makes them truly portable across any layout context.
- Use
container-type: inline-sizeand@containerrules to define component-level breakpoints based on content needs. - Container query units (
cqi,cqw,cqh) enable fluid sizing relative to the container for proportional spacing and typography. - Combine with media queries — media queries handle page structure, container queries handle component adaptation.
- Provide fallbacks using
@supportsfor browsers that haven't implemented container queries yet.
Container queries are the foundation of truly reusable, responsive components. Adopt them to build component libraries that work correctly in any layout context without context-specific overrides.