Type something to search...
A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout accordingly. That works well for page-level decisions, but it falls apart for components. A product card doesn't care how wide the screen is. It cares how much room it has. The same card might sit in a narrow sidebar, a three-column grid, and a full-width featured slot, all on the same page.

Container queries solve exactly this problem. They let an element respond to the size of its container instead of the viewport, which means you can build a component once and trust it to adapt wherever it's placed. Size container queries are supported in all major browsers, and they've quietly become one of the most important tools in modern CSS.

This guide covers how container queries work, the syntax in detail, container query units, style queries, and the practical patterns and pitfalls you'll run into.

Why Media Queries Aren't Enough

Here's the classic problem. You have a card component:

<article class="card">
  <img class="card-image" src="/images/lamp.jpg" alt="Brass desk lamp" />
  <div class="card-content">
    <h3 class="card-title">Brass Desk Lamp</h3>
    <p class="card-text">Warm light, solid base, and a flexible neck.</p>
    <a class="card-link" href="/products/lamp">View product</a>
  </div>
</article>

You want it to stack vertically when narrow and switch to a horizontal layout (image on the left) when there's enough space. With media queries, you'd write something like:

@media (min-width: 900px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

But at 900px the card might be inside a 300px sidebar, where the horizontal layout would be cramped. Or it might be in a full-width section on a 700px tablet, where it has plenty of room but stays stacked. The viewport width is a poor proxy for the space the component actually has. You end up writing modifier classes like .card--sidebar and .card--featured, coupling the component to every context it appears in.

The Basics: Defining a Container

Container queries work in two steps. First, you declare an element as a query container. Then, you write @container rules that style its descendants based on the container's size.

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

@container (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1rem;
  }
}
<div class="card-wrapper">
  <article class="card">...</article>
</div>

Now the card switches to its horizontal layout whenever its wrapper is at least 480px wide, regardless of the viewport.

Why you need a wrapper

An important rule: a container query can't style the container itself, only its descendants. The query checks the container's size, and if the rule could change the container's size, you'd get an infinite loop. That's why the pattern above uses a wrapper element. In many layouts, the grid cell or column that holds the card is a natural wrapper, so you won't always need an extra div.

container-type Values

The container-type property accepts three values:

  • inline-size creates a container that can be queried on its inline dimension (width, in horizontal writing modes). This is what you'll use almost all the time.
  • size allows queries on both inline and block dimensions (width and height). It applies size containment on both axes, which means the element's height no longer depends on its content. You must give it an explicit height or it will collapse to zero.
  • normal is the default. The element isn't a size container, but it can still be a target for style queries (covered later).

Stick with inline-size unless you have a specific reason to query height. Using size on an element without a defined height is one of the most common container query bugs.

Naming Containers

When containers are nested, an @container rule queries the nearest ancestor that is a query container. That's often correct, but sometimes you want to target a specific one. Use container-name:

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

.main-content {
  container-type: inline-size;
  container-name: main;
}

@container sidebar (min-width: 300px) {
  .widget {
    padding: 1.5rem;
  }
}

The container shorthand combines name and type, with the name first:

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

An element can have multiple names, separated by spaces: container-name: sidebar layout-column;.

Query Syntax

The condition inside @container supports the same range syntax as modern media queries:

@container (width > 400px) { ... }
@container (400px <= width <= 800px) { ... }
@container (min-width: 400px) and (max-width: 800px) { ... }
@container (orientation: landscape) { ... }
@container (aspect-ratio > 1) { ... }

You can combine conditions with and, or, and not:

@container card (width > 500px) and (height > 300px) {
  .card-title {
    font-size: 1.5rem;
  }
}

Remember that height, orientation, and aspect-ratio queries require container-type: size, since an inline-size container only exposes its width.

Container Query Units

Alongside queries, you get a set of units relative to the query container's dimensions:

UnitRelative to
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
cqminThe smaller of cqi and cqb
cqmaxThe larger of cqi and cqb

These are incredibly useful for fluid typography that scales with the component instead of the page:

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

.card-title {
  font-size: clamp(1.1rem, 4cqi + 0.5rem, 2rem);
}

The title grows as the card grows, but it's clamped between sensible limits. Prefer cqi over cqw: it follows the writing mode, so it behaves correctly in vertical text layouts.

If no ancestor is a query container, the units fall back to the small viewport units (svw, svh, and so on), so they won't break outright, but they won't do what you intended either.

A Complete Example: An Adaptive Product Card

Let's build a card with three layouts: stacked, horizontal, and a large "featured" version.

<ul class="product-grid">
  <li class="product-slot">
    <article class="product">
      <img
        class="product-image"
        src="/images/chair.jpg"
        alt="Oak dining chair"
      />
      <div class="product-body">
        <h3 class="product-title">Oak Dining Chair</h3>
        <p class="product-desc">Solid oak frame with a woven seat.</p>
        <p class="product-price">$189</p>
      </div>
    </article>
  </li>
  <!-- more slots -->
</ul>
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.5rem;
  list-style: none;
  padding: 0;
}

.product-slot {
  container: product / inline-size;
}

/* Default: stacked */
.product {
  display: grid;
  gap: 0.75rem;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  overflow: hidden;
}

.product-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.product-body {
  padding: 0 1rem 1rem;
}

.product-desc {
  display: none;
}

/* Medium: side by side */
@container product (width >= 420px) {
  .product {
    grid-template-columns: 40% 1fr;
    align-items: center;
  }

  .product-image {
    height: 100%;
    aspect-ratio: auto;
  }

  .product-body {
    padding: 1rem;
  }

  .product-desc {
    display: block;
  }
}

/* Large: featured */
@container product (width >= 700px) {
  .product-title {
    font-size: clamp(1.5rem, 3cqi, 2.25rem);
  }

  .product-price {
    font-size: 1.25rem;
    font-weight: 700;
  }
}

Put one of these cards in a narrow sidebar and it stacks. Put it in a full-width featured row and it becomes the large horizontal version. The component doesn't know or care about the page around it.

Style Queries

Size queries are only half the story. Style queries let you query the computed value of a custom property on a container:

.theme-panel {
  --variant: dark;
}

@container style(--variant: dark) {
  .button {
    background: #f8fafc;
    color: #0f172a;
  }
}

Every element is a style container by default, so you don't need container-type for style queries. An unnamed style query is evaluated against the element's parent; add a container name if you want to check a specific ancestor instead.

Style queries for custom properties are supported in Chromium-based browsers and recent Safari, while Firefox support has lagged behind. Check caniuse before shipping them for critical styling, and treat them as an enhancement:

/* Baseline: works everywhere */
.button {
  background: #0f172a;
  color: #f8fafc;
}

/* Enhancement where style queries are supported */
@container style(--variant: dark) {
  .button {
    background: #f8fafc;
    color: #0f172a;
  }
}

Browsers that don't understand the rule simply ignore it. Note that querying regular properties (like style(display: flex)) isn't supported anywhere yet; only custom properties work.

Browser Support and Fallbacks

Size container queries and container query units are supported in all current versions of Chrome, Edge, Firefox, and Safari, and have been for a few years. For most projects, you can use them without a fallback.

If you do need to support older browsers, use a feature query so the media query version only applies where container queries are missing:

/* Browsers without container queries get a viewport-based approximation */
@supports not (container-type: inline-size) {
  @media (min-width: 900px) {
    .product {
      grid-template-columns: 40% 1fr;
    }
  }
}

In practice, a simpler strategy is to design the default (unqueried) layout to be acceptable on its own. Older browsers get the stacked card, which works everywhere; modern ones get the enhanced version.

Common Pitfalls

Styling the container itself. A rule inside @container can't target the container element and expect the query to evaluate against it. Only descendants are affected by their ancestor container's query. If your styles aren't applying, check whether you're targeting the right element.

Using container-type: size without a height. The element collapses because size containment means its height no longer depends on its children. Use inline-size unless you truly need height queries.

Unexpected layout from inline-size containment. An inline-size container can't size itself from its content's width. That's usually fine for block elements that fill their parent, but if you put container-type: inline-size on an element that shrink-wraps (a flex item with flex: none, an inline-block, or a grid item in an auto track), it may collapse to zero width. Give it a definite width or let it stretch.

Querying the wrong container. Unnamed @container rules match the nearest query container ancestor. In deeply nested layouts, that may not be the one you think. Name your containers when nesting is involved.

Overusing containers. Not every element needs to be a container. Containment has a small cost and can affect layout. Apply it to meaningful layout slots: grid cells, sidebar regions, main columns.

Media Queries Still Have a Job

Container queries don't replace media queries. Use media queries for decisions about the whole page and the user's environment:

  • Page-level layout shifts (sidebar appears or disappears).
  • User preferences like prefers-reduced-motion and prefers-color-scheme.
  • Device capabilities like hover and pointer.
  • Print styles.

Use container queries for decisions about individual components and how they fit into the space they're given. A good rule of thumb: if the question is "how big is the screen?", use a media query. If it's "how big is this box?", use a container query.

Conclusion

Container queries shift responsive design from the page to the component. By declaring a container with container-type: inline-size and writing @container rules, you can build cards, widgets, and navigation blocks that adapt to any slot they're dropped into, without modifier classes or context-specific overrides. Container query units like cqi let typography and spacing scale with the component, and style queries add a new way to theme sections of a page, with support still maturing.

Start by converting one component that currently has a --sidebar or --compact modifier. Replace the modifier with a container query, and you'll immediately see how much simpler your CSS becomes.

Tags :
Share :

Related Posts

A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading
auto-fit vs. auto-fill in CSS Grid: What's the Difference?

auto-fit vs. auto-fill in CSS Grid: What's the Difference?

There's one line of CSS Grid that shows up in almost every modern card layout: grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

Continue Reading