Container Queries: The CSS Feature That Changed Everything
For years I wrote media queries that felt like educated guesses. "The sidebar is 300px, and on mobile the content area is full width, so if the viewport is less than 768px..." This arithmetic was fragile. Move a component to a different layout and the breakpoints broke.
Container queries eliminated that mental math. Instead of asking "how wide is the screen?" I now ask "how wide is my parent container?" The component adapts to its actual available space, not some assumed viewport width.
After using container queries across multiple production applications since they reached full browser support in 2023, I can say they've fundamentally changed how I think about responsive design.
The Problem Container Queries Solve
Consider a product card component. In a three-column grid, each card has roughly 400px of width. In a sidebar, the same card has 280px. With media queries, you'd write:
/* This assumes specific layout contexts */
@media (max-width: 768px) {
.product-card {
flex-direction: column;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.product-card {
flex-direction: row;
}
}But what if the sidebar appears on desktop? What if the grid changes to four columns? Every layout change requires recalculating breakpoints.
Container queries invert this logic:
.product-card-container {
container-type: inline-size;
}
@container (min-width: 350px) {
.product-card {
display: flex;
flex-direction: row;
}
}
@container (max-width: 349px) {
.product-card {
display: flex;
flex-direction: column;
}
}The card now responds to its container, not the viewport. Put it in a narrow sidebar—it stacks. Put it in a wide grid cell—it displays horizontally. No viewport calculations needed.
Browser Support in 2025
Container queries have excellent browser support. Chrome 105+, Firefox 110+, Safari 16+, and Edge 105+ all fully support size queries, style queries, and container units. That covers over 98% of global browser usage.
In my experience, you can use container queries in production without polyfills. For the rare legacy browser hit, your fallback is simply the default (non-queried) styles.
The Container Query API
Three CSS properties control container queries:
container-type
This property establishes a containment context. It tells the browser to track the element's dimensions.
.card-wrapper {
container-type: inline-size; /* Track width only */
}
.full-tracking {
container-type: size; /* Track width and height */
}inline-size is what you'll use 90% of the time. It tracks the inline dimension (width in horizontal writing modes) and enables @container queries based on width.
container-name
When you have nested containers, naming them prevents query conflicts:
.sidebar {
container-name: sidebar;
container-type: inline-size;
}
.main-content {
container-name: main;
container-type: inline-size;
}
/* Target specific container */
@container sidebar (min-width: 300px) {
.widget {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@container main (min-width: 600px) {
.article-card {
flex-direction: row;
}
}container (shorthand)
Combine name and type in one declaration:
.panel {
container: panel / inline-size;
}This is equivalent to:
.panel {
container-name: panel;
container-type: inline-size;
}Container Query Syntax
The @container rule works similarly to @media:
/* Anonymous container (nearest ancestor with container-type) */
@container (min-width: 400px) {
.child {
padding: 2rem;
}
}
/* Named container */
@container card-container (min-width: 400px) {
.card-content {
display: flex;
}
}
/* Multiple conditions */
@container (min-width: 300px) and (max-width: 600px) {
.element {
font-size: 1.125rem;
}
}Container Query Units
Container queries introduced new CSS units that scale relative to the container:
| Unit | Description |
|------|-------------|
| cqw | 1% of container's width |
| cqh | 1% of container's height |
| cqi | 1% of container's inline size |
| cqb | 1% of container's block size |
| cqmin | Smaller of cqi or cqb |
| cqmax | Larger of cqi or cqb |
These units enable fluid scaling without breakpoints:
.card-wrapper {
container-type: inline-size;
}
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
.card-image {
border-radius: 2cqw;
}
.card-padding {
padding: 3cqw 4cqw;
}In my experience, cqi (inline size) is the most useful. It scales elements proportionally to the container's width regardless of the container's actual pixel dimensions.
When to Use Container Queries vs Media Queries
Both have their place. Here's how I decide:
Use container queries for:
- Reusable components (cards, widgets, navigation items)
- Components that appear in multiple layout contexts
- Component libraries and design systems
- Nested layouts where parent width varies
Use media queries for:
- Page-level layout changes (switching from sidebar to stacked)
- Global navigation changes (hamburger menu on mobile)
- Typography scale changes based on device class
- Print stylesheets
Hybrid approach (what I do most often):
/* Media query for page-level layout */
@media (min-width: 1024px) {
.page-layout {
display: grid;
grid-template-columns: 1fr 320px;
}
}
/* Container query for component-level adaptation */
.sidebar-widget-container {
container-type: inline-size;
}
@container (min-width: 280px) {
.widget-content {
display: flex;
gap: 1rem;
}
}The page layout uses media queries. The components within use container queries. Each tool handles its appropriate scope.
Real-World Patterns
Adaptive Card Component
This pattern handles cards that might appear in a grid, sidebar, or modal:
.card-container {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-image {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
align-items: flex-start;
}
.card-image {
width: 40%;
flex-shrink: 0;
}
.card-content {
flex: 1;
}
}
@container (min-width: 600px) {
.card-image {
width: 35%;
}
.card-title {
font-size: 1.5rem;
}
}Dashboard Widget Grid
Dashboard widgets need to adapt to their panel sizes:
.dashboard-panel {
container: dashboard-panel / inline-size;
}
.metric-widget {
display: flex;
flex-direction: column;
padding: 4cqw;
}
.metric-value {
font-size: clamp(1.5rem, 8cqi, 3rem);
font-weight: 700;
}
.metric-label {
font-size: clamp(0.75rem, 3cqi, 1rem);
color: #666;
}
@container dashboard-panel (min-width: 300px) {
.metric-widget {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}Navigation That Adapts
Navigation items can switch between icon-only and full labels:
.nav-container {
container-type: inline-size;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
}
.nav-label {
display: none;
}
.nav-icon {
width: 1.5rem;
height: 1.5rem;
}
@container (min-width: 200px) {
.nav-label {
display: inline;
}
}Style Queries
Container queries can also query CSS custom property values, not just dimensions. Every element is a style container by default—no container-type needed:
.theme-wrapper {
--theme: light;
}
.dark-section {
--theme: dark;
}
@container style(--theme: dark) {
.button {
background: #333;
color: #fff;
}
.card {
background: #1a1a1a;
border-color: #333;
}
}
@container style(--theme: light) {
.button {
background: #fff;
color: #333;
border: 1px solid #ddd;
}
.card {
background: #fff;
border-color: #e5e5e5;
}
}This enables theme scoping without JavaScript or class toggling. Set a custom property on a container, and descendants respond.
Performance Considerations
Container queries use CSS Containment under the hood, which actually improves performance. The browser isolates the contained subtree, limiting the scope of style recalculations.
In my testing with Chrome DevTools, container queries outperform equivalent JavaScript-based solutions (like ResizeObserver with class toggling) by 20-30% in repaint time. The browser handles everything natively.
A few performance tips:
- Use
inline-sizeoversizewhen you only need width queries - Name containers in complex nested layouts to avoid unnecessary query matching
- Container units (
cqw,cqi) are more performant than breakpoint-heavy queries for fluid scaling
Integration with React and Next.js
Container queries work seamlessly with component frameworks. No special setup needed—just CSS:
// components/ProductCard.tsx
import styles from './ProductCard.module.css';
interface ProductCardProps {
title: string;
price: number;
image: string;
}
export function ProductCard({ title, price, image }: ProductCardProps) {
return (
<div className={styles.container}>
<article className={styles.card}>
<img src={image} alt={title} className={styles.image} />
<div className={styles.content}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.price}>${price}</p>
</div>
</article>
</div>
);
}/* ProductCard.module.css */
.container {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: #fff;
border-radius: 0.5rem;
}
.image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 0.25rem;
}
@container (min-width: 350px) {
.card {
flex-direction: row;
}
.image {
width: 40%;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
}With Tailwind CSS, you can use the container query plugin or inline styles:
<div className="@container">
<div className="flex flex-col @[350px]:flex-row gap-3">
{/* content */}
</div>
</div>Migration from Media Queries
If you're migrating existing components, here's my approach:
- Identify components that appear in multiple layout contexts
- Wrap each component instance in a container div with
container-type: inline-size - Convert
@mediaqueries to@containerqueries using the container's expected sizes - Test the component in all its layout contexts
- Remove viewport-specific media queries from the component
The key insight: component styles should use container queries, page layout styles should use media queries. This separation makes components truly portable.
For projects using React Server Components or Next.js 16's Cache Components, container queries work without any modifications—they're pure CSS and require no client-side JavaScript.