CSS Container Queries in Practice — Component-Level Responsive Design

Media queries can't see the box your component lives in. Container queries can. Here's how to use them in real projects, with patterns and live demos you can poke at.

Why media queries aren't always enough

Here's a problem you've probably hit: a card component looks great in the main column, but the moment you drop it into a narrow sidebar, the layout falls apart. The screen is wide, but the spot the card lives in is narrow — and media queries can't tell the difference. They only know about the viewport.

That's exactly the gap container queries fill. Instead of asking "how wide is the screen?", a component asks "how wide is my parent?". It's a small shift in perspective, but it changes how you design responsive components from the ground up. The component starts to be aware of where it's placed, and adapts itself accordingly.

📌 Key point
Media queries are great for switching the overall page layout. Container queries are great for switching how a component looks inside that layout. They don't replace each other — in real projects, you use both, each for what it's good at.

The minimum setup: declare a container, then query it

The mental model is straightforward once you've seen it. First, mark a parent element as a container. Then, write rules for its children using @container. That's the whole pattern.

Bare-minimum container query
/* 1. Declare the parent as a container */
.card {
  container-type: inline-size;
  container-name: card;
}

/* 2. Style the children based on the container's width */
@container card (min-width: 480px) {
  .card__inner {
    flex-direction: row;
  }
}

container-type: inline-size tells the browser, "track the inline (horizontal) size of this element so children can query it." The container-name is optional, but in real projects with nested containers, naming them saves you from a lot of confusion later.

▶ Live Demo — Resize the box, watch the card flip

Drag the bottom-right corner of the box below. Once it crosses 480px, the card switches to a horizontal layout — even though your screen size never changed.

↔ Drag to resize

A card that knows where it lives

This card responds to its parent's width, not the viewport. Whether it sits in the main column or a sidebar, it figures out the right shape on its own.

That demo is the whole idea in miniature. The viewport hasn't changed at all, but the card adapts because the box around it changed. Once you see this clicking in action, you stop thinking in screens and start thinking in components.

A real-world pattern: same card, different column counts

The place container queries pay off the most is grids of reusable cards. With pure media queries, you end up coupling the grid's column count to each card's internal layout — and that breaks the moment someone reuses the card somewhere else. The card gets dropped into a sidebar, and suddenly your "tablet" styles are kicking in even though there's no room for them.

❌ Tying the card to the viewport
/* This breaks the moment the card lands in a narrow column */
@media (min-width: 768px) {
  .card__inner { flex-direction: row; }
}
✅ Letting the card decide for itself
.card { container-type: inline-size; }

@container (min-width: 480px) {
  .card__inner { flex-direction: row; }
  .card__title { font-size: 22px; }
}
▶ Live Demo — One card, one stylesheet, two layouts

When the viewport is wide enough, these cards sit two-up. At that point, each card's own width drops below 480px — so they automatically fall back to the stacked layout. "Wider screen" doesn't always mean "wider card."

Card A

Two columns means each card is narrower, so it stacks. The card decides this on its own.

Card B

Same component, same CSS. Behavior depends entirely on where it's been placed.

Notice that there's only one set of card styles here. The page can put these cards anywhere — one column, two columns, sidebar, modal — and the card always picks the right shape. That's the kind of decoupling that makes design systems actually scalable.

💡 From experience
A rule of thumb that's worked well for me: media queries handle the page-level grid (how many columns there are). Container queries handle the inside of each component. When you split responsibilities this way, moving a component to a new location stops being scary.

Going further: scale typography with the cqi unit

Container queries also bring new units. The most useful one is cqi — one percent of the container's inline size. Think of it as vw, but scoped to the container instead of the viewport. It lets you scale things like font size smoothly based on where the component is placed.

clamp() + cqi for fluid headings
.box {
  container-type: inline-size;
}
.box__title {
  /* min 18px, scales with 6% of container width, max 36px */
  font-size: clamp(18px, 6cqi, 36px);
}
▶ Live Demo — Typography that follows the container

Resize the box below. The heading grows and shrinks based on the container's width, not the viewport's. The clamp() keeps it from going too small or too big.

↔ Drag to resize

A title sized by its container

That's cqi at work. Place this component anywhere and the typography scales naturally with the available space.

There are siblings — cqw (width), cqh (height), cqb (block), plus cqmin and cqmax — but honestly, in day-to-day work I reach for cqi almost every time. For horizontal-flow languages, cqi equals cqw, so starting with just that one unit covers most cases.

⚠️ Watch out
Setting container-type: inline-size creates a new containing context for the element. That can affect things like position: fixed children or height: 100% calculations inside. The fix is simple: don't sprinkle container-type everywhere — only put it on components that actually need to be queried.

Adoption tips and browser support

Container queries went stable across all major browsers back in 2023. Chrome, Safari, Firefox, and Edge all support them today, so for most projects you can use them without a second thought. If you're stuck supporting older browsers, @supports gives you a clean way to layer the feature in.

Progressive enhancement with @supports
/* Default: stacked layout — works even without container query support */
.card__inner { flex-direction: column; }

@supports (container-type: inline-size) {
  .card { container-type: inline-size; }

  @container (min-width: 480px) {
    .card__inner { flex-direction: row; }
  }
}

The other thing worth thinking about is which elements you actually mark as containers. If every component declares itself a container, you end up with a mess of overlapping query contexts and hard-to-trace bugs. A cleaner rule: only the outermost wrapper of a reusable component becomes a container. That keeps the system predictable.

Naming is the last piece. On a team, give your containers names that describe their role — card, media, sidebar, and so on. Anonymous containers work fine for small cases, but once you have nested components, named queries are much easier to read and reason about. Spend the extra few characters; future-you will thank you.

Takeaways

  • Container queries let elements respond to their parent container's size instead of the viewport, making them ideal for component-level responsive design.
  • The setup is two steps: add container-type: inline-size to the parent, then write @container (min-width: ...) rules for its children.
  • The biggest payoff is reusable components — the same card can sit in a main column or a sidebar and adapt without page-specific overrides.
  • Pair them with media queries: page-level layout uses media queries, internal component layout uses container queries.
  • The cqi unit lets typography and spacing scale smoothly with the container's width, scoped to where the component lives.
  • container-type creates a new containing context, so use it deliberately on the outermost wrapper of reusable components rather than scattering it everywhere.
  • Browser support has been solid since 2023. For older targets, wrap the feature in @supports and ship the fallback as the default.