Skip to content

Web Development · CSS

CSS Container Queries in Production: Components That Adapt to Their Context

Media queries ask 'how wide is the viewport?' Container queries ask 'how wide is my parent?' That shift changes how you think about reusable components.

Anurag Verma

Anurag Verma

7 min read

CSS Container Queries in Production: Components That Adapt to Their Context

Sponsored

Share

The card component is the classic example. You build it to display a user profile: avatar on the left, name and bio on the right. It looks great in the sidebar. Then design asks for the same card in the main feed, which is twice as wide. You add a breakpoint. Then marketing wants the card in a three-column grid on desktop and a single column on mobile. You add more breakpoints. Then the same card gets used in a modal, in a dashboard widget, in an email template renderer.

After a while, the component has a dozen media queries and the maintainer needs to know every context it might appear in to know what it will look like.

Container queries solve this by asking a different question. Instead of “how wide is the viewport right now?”, they ask “how wide is the element I’m inside?” The component responds to its container, not the world. That makes it genuinely portable.

What Container Queries Actually Are

Browser support has been solid since late 2023. Chrome, Firefox, Safari — all current versions support both size container queries and container query units. You do not need a polyfill for any production target that excludes IE.

The API has two parts: declaring a container, and writing queries against it.

Declaring a container:

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

container-type: inline-size tells the browser: “measure this element’s inline dimension (width in horizontal writing modes) and expose it to container queries from children.”

Other values:

  • container-type: size — measures both inline and block dimensions (width and height)
  • container-type: normal — default, no size containment, but style queries still work

Writing a container query:

.card {
  display: flex;
  flex-direction: column;
}

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

When the container is at least 400px wide, the card switches from stacked to side-by-side layout. No media query. No knowledge of where the card lives.

Named Containers

If you have nested containers, a query runs against the nearest ancestor that is a container. To target a specific ancestor, name it:

.dashboard {
  container-type: inline-size;
  container-name: dashboard;
}

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

/* Runs against the dashboard container specifically */
@container dashboard (min-width: 900px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

/* Runs against the sidebar container */
@container sidebar (max-width: 240px) {
  .card {
    padding: 8px;
    font-size: 0.875rem;
  }
}

This becomes useful in complex layouts where a component appears in multiple containers and should behave differently in each.

Container Query Units

Along with the @container rule, there are six new length units:

UnitWhat it measures
cqw1% of the container’s width
cqh1% of the container’s height
cqi1% of the container’s inline size
cqb1% of the container’s block size
cqminSmaller of cqi and cqb
cqmaxLarger of cqi and cqb

Think of them as the container-relative equivalent of vw and vh. They let typography and spacing scale proportionally to the container rather than the viewport:

.card-title {
  font-size: clamp(1rem, 4cqi, 1.5rem);
  padding: 2cqi 3cqi;
}

The title scales between 1rem and 1.5rem based on 4% of the container’s inline width, regardless of viewport size. This is genuinely useful in sidebar widgets where the container can be 200px or 600px depending on the layout.

A Real Component Example

Here is a media card — image, title, description, tags — that works in any context without media queries:

<div class="media-card-container">
  <div class="media-card">
    <img class="media-card__image" src="..." alt="..." />
    <div class="media-card__body">
      <h3 class="media-card__title">...</h3>
      <p class="media-card__description">...</p>
      <div class="media-card__tags">...</div>
    </div>
  </div>
</div>
.media-card-container {
  container-type: inline-size;
  container-name: media-card;
}

/* Default: narrow layout */
.media-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
}

.media-card__image {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 8px;
}

.media-card__title {
  font-size: clamp(1rem, 5cqi, 1.25rem);
  margin: 0;
}

.media-card__description {
  font-size: 0.875rem;
  display: none;
}

.media-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* Medium container: show description */
@container media-card (min-width: 320px) {
  .media-card__description {
    display: block;
  }
}

/* Wide container: side-by-side layout */
@container media-card (min-width: 500px) {
  .media-card {
    flex-direction: row;
    align-items: flex-start;
  }

  .media-card__image {
    width: 200px;
    height: 160px;
    flex-shrink: 0;
  }
}

/* Very wide: two-column tag layout */
@container media-card (min-width: 700px) {
  .media-card__tags {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

Drop this component into a sidebar, a main feed, or a three-column grid. It adapts to wherever it lands. The consuming context never needs to know what sizes the component has states for.

Container Queries and CSS Grid

Container queries pair well with Grid for layout patterns where children need to respond to the grid’s column assignment.

.post-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
}

.post-grid-item {
  container-type: inline-size;
}

/* A featured post spans two columns; its children should respond differently */
.post-grid-item--featured {
  grid-column: span 2;
}

@container (min-width: 520px) {
  .post-card {
    display: grid;
    grid-template-columns: 240px 1fr;
  }

  .post-card__image {
    height: 100%;
  }
}

Without container queries, you would need to add a class to featured cards and write separate CSS rules for them. With container queries, the card responds to the space it actually has, whether that comes from being featured, from a wider viewport, or from being in a less constrained grid.

Style Container Queries

Beyond size queries, there is a newer feature: style queries. They let children respond to the computed value of a custom property on the container.

.card-wrapper {
  container-type: normal;   /* No size containment needed for style queries */
}

/* When a parent sets this custom property */
.theme-dark {
  --card-theme: dark;
}

@container style(--card-theme: dark) {
  .card {
    background: #1a1a1a;
    color: #f0f0f0;
    border-color: #333;
  }
}

Browser support for style queries is more limited than size queries. Chrome and Edge support it. Firefox is in progress. Safari has partial support. Use with a fallback in 2026.

The practical value: a component library can expose a theme API via custom properties on container elements, and child components respond to those properties rather than needing explicit class names passed through. Less prop drilling for theming.

What Container Queries Don’t Replace

Media queries still handle:

  • Root-level layouts: the main grid, sidebar visibility, navigation collapse
  • Typography on body or :root: base font size relative to viewport
  • Print styles
  • Breakpoints for page-level structure where there is no container above the thing you are styling

The right model: media queries define the page skeleton, container queries define how components fill the skeleton’s cells. Neither is redundant.

Performance

Container queries have no meaningful performance cost over media queries. The browser recalculates container sizes on layout, which it is already doing. The only consideration: avoid deeply nested containers or situations where a container query change triggers a layout that changes the container’s size, creating a feedback loop. This is caught by the containment rules — an element cannot query its own size, only its container’s, which prevents cycles.

The Practical Upgrade Path

If you have an existing design system or component library, the migration is low risk:

  1. Add container-type: inline-size to the wrapper elements of components that currently have viewport breakpoints baked in.
  2. Convert the component’s internal media queries to @container queries with the same breakpoints.
  3. Test across every context the component appears in.
  4. Remove context-specific override classes that were compensating for the old approach.

The component does not need to know its context. Its container tells it what to be.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored