CSS Container Queries Tutorial: 3 Patterns That Beat Media Queries

2026-04-13 · Nico Brandt

Container queries have 94% browser support. 86% of CSS developers know they exist. Only 41% have actually used them.

That gap isn’t a knowledge problem. It’s a patterns problem. You’ve read the explainers, you understand the concept, but when you sit down to write CSS you still reach for @media. Because nobody showed you the specific situations where container queries are genuinely better — or told you honestly when media queries are still correct.

Three patterns, a migration path, and a decision framework. No encyclopedia. Let’s go.

The Problem Media Queries Can’t Solve

You build a card component. Horizontal layout, image left, text right. It looks great in the main content area at desktop widths. Then someone drops it into a 300px sidebar.

Your media query says the viewport is 1440px wide. It applies the horizontal layout. The card is crushed into illegibility.

This is the core failure: media queries measure the viewport, but your component lives inside a container that could be any size. The viewport doesn’t know your sidebar exists.

The css container query syntax to fix this is small enough to fit on a sticky note:

.card-wrapper {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card { display: flex; gap: 1.5rem; }
}

Parent gets container-type: inline-size. Child queries the parent’s width instead of the viewport. That’s the entire API.

One rule to internalize early: a container cannot query itself. You always measure an ancestor. Sometimes that means adding a wrapper element — though the :has() selector can eliminate that extra div in some cases.

So where does this actually matter in production?

Pattern 1: The Card That Goes Anywhere

Without container queries, a card that lives in multiple contexts accumulates parent-aware selectors:

.card { /* stacked by default */ }
@media (min-width: 768px) {
  .main-content .card { display: flex; }
}
.sidebar .card { /* stays stacked */ }
.hero .card { display: flex; font-size: 1.25rem; }

Every new context needs a new rule. Modal? New selector. Dashboard widget? Another one. The component has leaked knowledge of every layout it might live in — the CSS equivalent of tight coupling.

Container queries invert this entirely:

.card-container { container-type: inline-size; }

.card { /* stacked by default */ }

@container (min-width: 500px) {
  .card { display: flex; gap: 1.5rem; }
}

@container (min-width: 700px) {
  .card { font-size: 1.125rem; }
}

The card doesn’t know where it lives. Sidebar, main content, modal, dashboard — it adapts to whatever space it gets. Zero context classes. One set of responsive components CSS rules. Drop it anywhere and it works.

This is component-level responsive design, and it’s the pattern that makes the biggest immediate difference in real codebases. If your project has a performance checklist that flags layout shift, you’ll appreciate that these adaptations happen in pure CSS — no JS resize observers janking up the paint.

Cards are the obvious win. But container queries solve more than cards.

Pattern 2: The Sidebar-Aware Component

A navigation panel lives inside a collapsible sidebar. Wide sidebar: show icons and labels. Collapsed: icons only. Media queries can’t detect sidebar state — the viewport didn’t change when the user toggled a panel.

.sidebar { container-type: inline-size; }

.nav-item span { display: none; }

@container (min-width: 200px) {
  .nav-item span { display: inline; }
}

The sidebar is the container. Nav items query it. Sidebar collapses, labels vanish. Expands, labels return. No JavaScript class toggling. No resize observers.

This pattern extends to anything in a resizable panel: file explorers, chat sidebars, admin dashboards. Anywhere a component’s available space changes independently of the viewport.

Here’s where container query units get interesting. Instead of breakpoint jumps, cqi (container query inline) gives you fluid sizing:

.nav-item {
  font-size: clamp(0.75rem, 3cqi, 1rem);
  padding: clamp(0.25rem, 1.5cqi, 0.75rem);
}

Font size and padding scale smoothly with the container width. No breakpoints at all. Which raises the question: can you build components that have zero breakpoints?

Pattern 3: Fluid Dashboard Widgets

Dashboard stat widgets need to work at any size from 150px to 600px wide. Breakpoints don’t cut it — too many possible sizes, and hard jumps between layouts feel janky.

Container query units with clamp() handle this:

.stat-wrapper { container-type: inline-size; }

.stat-widget {
  padding: clamp(0.5rem, 3cqi, 2rem);
  gap: clamp(0.25rem, 1.5cqi, 1rem);
}

.stat-number {
  font-size: clamp(1.25rem, 8cqi, 3rem);
}

.stat-label {
  font-size: clamp(0.7rem, 2.5cqi, 0.9rem);
}

Number, label, padding, and spacing all scale fluidly with the container’s inline size. One component definition works from a compact mobile tile to a wide desktop panel. No breakpoints. No modifier classes.

One gotcha: container query units with box-sizing: border-box calculate based on the content box, not the border box. If your padding-heavy component looks off, that mismatch is why.

Three patterns. Three real problems solved. But you have a codebase full of media queries right now — and you’re wondering how to actually start.

Migrating a Component in 4 Steps

Step 1: Identify the candidate. Ask one question: does this component appear in multiple layout contexts? If it only ever lives in one place at one width, container queries add complexity for zero benefit. If it moves between sidebar, main content, modals, or different page layouts — it’s a candidate.

Step 2: Declare the container. Add container-type: inline-size to the parent element. If there’s no suitable parent, add a wrapper div. This creates a containment context — the browser guarantees the container’s size won’t depend on its children’s sizes, which is how it avoids infinite layout loops.

Step 3: Convert @media to @container. Replace viewport breakpoints with container breakpoints. The values will be different — a 600px container breakpoint is not the same as a 600px viewport breakpoint. Your main content area might be 800px when the viewport is 1200px. Test in DevTools and adjust.

Step 4: Clean up. Delete parent-context selectors (.sidebar .card), modifier classes (.card--compact), and any JavaScript that toggles layout classes based on parent width. This is the satisfying part — you’re removing code, not adding it.

Production gotchas: container-type creates a new stacking context and containment context. Flexbox items with container-type may collapse to zero width without explicit sizing. Grid items as containers need explicit dimensions — the grid algorithm and containment don’t always agree. Migrate incrementally and test each component in isolation.

Now you have the patterns and the migration path. The remaining question: should you migrate everything?

When Media Queries Are Still the Right Call

No. Don’t migrate everything.

Viewport-level layout — two-column versus single-column page layout is fundamentally about the viewport. Media queries are correct here. The page layout IS about the screen.

User preference queriesprefers-reduced-motion, prefers-color-scheme, prefers-contrast. These respond to user settings, not container sizes. Container queries can’t access them. When you’re choosing CSS approaches that respect user preferences, media queries remain the tool.

Print styles@media print isn’t going anywhere.

Fixed-width contexts — if a component only ever renders at one size in one place, a container query adds abstraction for zero benefit.

The decision rule: if it’s about the viewport or the user → media query. If it’s about the component’s available space → container query.

The Decision in One Line

Media queries for page layout and user preferences. Container queries for reusable components that move between contexts.

The reason 59% of developers haven’t used container queries isn’t that the API is hard — the syntax is smaller than most CSS grid setups. It’s that the css container queries tutorials out there explain the what without showing the where. Three patterns: cards that adapt anywhere, sidebar-aware components, fluid dashboard widgets. That covers the majority of cases where container queries genuinely beat media queries.

Pick one component in your current project that appears in more than one layout context. Migrate it using the four steps above. That’s your proof of concept — and once you see a component that truly doesn’t care where it lives, you won’t want to go back to scattering .sidebar .card selectors across your stylesheets.