The Basic Idea Behind Container Queries

For years, the most-requested CSS feature was container queries. They’ve now been supported in all major browsers for nearly two years, yet adoption has remained surprisingly low. Most developers have only run a few small experiments with them.

One big reason is confusion around how they actually work. Container queries aren’t as straightforward as media queries. To use them effectively, you need to understand their constraints. Once you have the right mental model, they become a genuinely useful tool.

Media queries have been the standard approach to responsive design for roughly two decades. They typically depend on global properties like viewport width:

@media (min-width: 40rem) {
  .mobile-only {
    display: none;
  }
}

Media queries are fine for global conditions, but they can’t handle local needs — like applying CSS conditionally based on the size of an element’s container.

Consider a ProfileCard component that displays key user information. When placed in a narrow column, the information stacks vertically. But when the component has more breathing room, it could shift to a side-by-side layout instead.

In some cases, media queries work if the card scales with the viewport, but they won’t always be appropriate. If cards are arranged in a flex grid, each card needs to pick a layout based on the space available to that individual card — which has nothing to do with the viewport size.

Here’s how container queries solve this:

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

.child {
  /* Narrow layout stuff here */

  @container (min-width: 15rem) {
    /* Wide layout stuff here */
  }
}

CSS nesting can place the @container at-rule right inside the component block, keeping all related declarations together.

The key question is: why do we need a wrapping element with a container-type property? Understanding this requires digging into the core challenge that made container queries so hard to implement.

Solving the Impossible Problem

Developers have wanted container queries for two decades. The CSS Working Group’s response has always been the same: implementation is impossible.

A concrete example makes this clear. Notice this: an element with fit-content sizing dynamically grows or shrinks based on its content:

<style>  p {    width: fit-content;    outline-style: solid;  }</style><p>  <strong>Hello World!</strong></p>

Now imagine conditionally applying CSS based on the container’s size:

p {
  width: fit-content;

  @container (max-width: 10rem) {
    strong {
      font-size: 3rem;
    }
  }
}

Here’s the problem: when we change an element’s font-size, we influence both its height and its width. If the container starts out 10rem wide or smaller, applying the font-size change could push it beyond 10rem — thereby invalidating the condition that triggered the change in the first place.

This creates what would be an infinite feedback loop, flickering endlessly between matched and unmatched states. This is why the CSS Working Group claimed it literally couldn’t be done.

Media queries avoid this issue entirely because their conditions rely on immutable global states — like viewport dimensions or the color scheme of the operating system, which CSS can’t change. But container sizes are fully mutable by our own styles, making the loop possible.

The Revolutionary Containment API

The breakthrough arrived with the Containment API, which was introduced a few years back. It allows explicit marking of certain DOM slices as self-contained — meaning their content won’t leak and influence styling elsewhere in the document:

an axolotl in an aquarium

By default, a parent grows and shrinks to fit its children, which is exactly what creates the infinite loop problem. But, by applying contain: size, you sever that connection. This tells the browser the height of the container should not depend on its content at all.

This API was designed with performance in mind, so the browser could safely skip layout recalculations for that entire subtree. It’s conceptually a lot like React.memo() for CSS, being used to skip recalculations that we know are pointless. Though not everyday-declarations, this API allowed us to solve the so-called impossible problem of container queries. It paved the way to definitively break the loop by preventing containers from dynamically responding to their content.

Your First Container Query

Here’s a minimal “hello world” implementation:

<style>  section {    container-type: size;  }  @container (max-width: 12rem) {    p {      font-weight: bold;      color: red;    }  }</style><section>  <p>    This text becomes bold and red in small containers.  </p></section>

We begin by declaring the element as a container. This allows descendants to measure it. Then, we declare a container query to apply styles when the container is 12rem wide or less:

<style>  section {    container-type: inline-size;    background-color: peachpuff;    border: 2px solid;  }  @container (max-width: 12rem) {    p {      font-weight: bold;      color: red;    }  }</style><section>  <p>    Yay, it works! The parent’s height will change dynamically to accommodate its content, and the container query still fires when the container is resized!  </p></section>

By default, width on the web is a bit special: elements tend to expand and fill available space. Height, however, is driven “shrinkwrap” style around content. If we use full container-type: size, the child’s content is ignored entirely, making the parent collapse to zero height and shedding any background color.

The fix is container-type: inline-size. The phrase “inline-size” refers to the inline dimension, usually width. This means only the container’s width becomes independent from content, but its height still behaves as normal and don’t collapse unexpectedly.

The golden rule with container queries: you cannot style what you measure. That is, container-type: inline-size means container queries can use conditions like min-width or max-width, but not min-height or max-height.

Where container queries stand today

Container queries are now available in every major browser engine. Safari shipped support in version 16 (September 2022), Chrome and Edge followed with version 105 (August 2022), and Firefox landed support in version 110 (February 2023). As of late 2024, global support sits around 93%.

The examples in this article rely on native CSS Nesting, a feature that only recently became part of the CSS spec after years of being a preprocessor staple. If you are working without a build step, review the browser support for native nesting before adopting it.

A shift in the responsive mindset

Despite being available in all major browsers for over two years, container queries remain surprisingly rare in production code. Most developers I talk to have not integrated them into their day-to-day workflow.

Part of that is complexity—container queries are genuinely more involved than viewport-based media queries. But I suspect the larger obstacle is cultural. For nearly two decades, the working relationship between designers and developers has been built on an implicit constraint: responsive behavior is dictated by the viewport, and nothing else. Mockups are delivered with breakpoints in mind, and that expectation has shaped what designers ask for and what developers build.

Most designers are not aware that component-level responsiveness is now possible. Bringing container queries into a project means introducing that capability during the design phase, not just at implementation time. If you work with a designer, it is worth having that conversation.

For those of us who handle both design and development, there is no excuse. When I redesigned my blog over the summer, I deliberately chose container queries wherever they made sense. Once I started thinking in terms of containers, opportunities to use them appeared everywhere.

For a deeper look at what container queries unlock beyond the basics, including new design patterns they enable, see my follow-up post "Container Queries Unleashed".