CSS Container Queries: Building Truly Responsive Components
Stop designing for viewport widths. Learn how container queries let you build components that respond to their actual container size, not the screen.
CSS Container Queries: Building Truly Responsive Components
For years, responsive design meant one thing: media queries tied to viewport width. Your component looked one way on mobile, another on desktop, and that was it.
But here's the problem: a component doesn't care about the viewport. It cares about the space it actually has. A sidebar widget on a 1920px screen might have less space than the same widget on a 768px tablet. Viewport-based media queries can't handle this.
Container queries change that.
The Old Problem with Viewport Media Queries
Consider a card component. You design it to look great on mobile (one-column layout), tablet (still one column), and desktop (two columns).
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 1024px) {
.card {
grid-template-columns: 1fr 1fr;
}
}
This works fine in isolation. But what if that card appears in a sidebar that's only 300px wide on a desktop? It'll still try to use the two-column layout and everything breaks.
With container queries, the component itself controls how it responds to available space.
How Container Queries Work
First, you define a container on a parent element:
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
The container-type: inline-size watches the width of that element (inline-size is the horizontal axis).
Then, inside child elements, you use @container instead of @media:
.card {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@container sidebar (min-width: 400px) {
.card {
grid-template-columns: 1fr 1fr;
}
}
Now the card responds to the .sidebar width, not the viewport. If the sidebar is 300px, it's one column. If it's 600px, it's two columns. Anywhere the card appears with this container, it works.
Named vs. Unnamed Containers
You can use named containers (like sidebar above) or unnamed:
.container {
container-type: inline-size;
}
@container (min-width: 500px) {
.item {
padding: 2rem;
}
}
When unnamed, the query targets the nearest ancestor with container-type set.
Container Query Units
Inside @container queries, you get special units:
- cqw - Container query width (1cqw = 1% of container width)
- cqh - Container query height (1cqh = 1% of container height)
- cqi - Container query inline size
- cqb - Container query block size
This is powerful for fluid typography and spacing:
.card {
padding: clamp(1rem, 5cqw, 2rem);
font-size: clamp(0.875rem, 2.5cqw, 1.125rem);
}
The padding scales with the container width, clamped between 1rem and 2rem. Perfect.
Style Queries (Advanced)
Container queries go beyond size. You can query custom properties:
.card {
container-type: inline-size;
--theme: light;
}
.card.dark {
--theme: dark;
}
.icon {
color: black;
}
@container style(--theme: dark) {
.icon {
color: white;
}
}
This is experimental but incredibly powerful for component theming.
Browser Support & Progressive Enhancement
Container queries have excellent support in modern browsers (Chrome 105+, Firefox 110+, Safari 16+). For older browsers, the queries simply don't apply, and your component falls back to its default styles.
This is perfect for progressive enhancement:
.card {
/* Default single-column layout for all browsers */
display: grid;
grid-template-columns: 1fr;
}
@container (min-width: 500px) {
/* Modern browsers with container query support use this */
.card {
grid-template-columns: 1fr 1fr;
}
}
Older browsers show the single-column layout. Modern browsers get the enhanced version.
Real-World Example: A Flexible Card Grid
Here's a practical example: a card grid that adapts to its container, not the viewport.
.card-grid {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.card {
background: var(--surface);
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
flex-direction: column;
}
/* On larger containers, use grid for content */
@container (min-width: 600px) {
.card {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
}
.card-image {
grid-row: 1 / -1;
width: 100px;
height: 100px;
}
}
/* Extra space? Make the title bigger */
@container (min-width: 800px) {
.card-title {
font-size: 1.25rem;
}
}
The same grid component works in a wide hero section, a narrow sidebar, and everything in between.
When to Use Container Queries
Use them when:
- Building reusable components that appear in different layouts
- You need responsive behavior based on container space, not viewport
- Components might be nested or placed in dynamic layouts
- You want truly flexible component libraries
Skip them when:
- A simple media query covers your use case
- You're only changing breakpoints once at the page level
- Browser support is a blocker for your audience
The Future of Responsive Design
Container queries flip the script from "design for screens" to "design for space." This is closer to how responsive design should work from the ground up.
Combined with modern CSS features like clamp(), cascade layers, and CSS Grid, you can build components that are genuinely responsive, resilient, and reusable.
Your components are finally smarter than your viewport.