Beyond Viewport-Based Thinking

Media queries have been the backbone of responsive design for years, but they suffer from a fundamental limitation: they are completely context-agnostic. They only know about the viewport, not about the containers that actually hold your content. An element squeezed into a narrow column looks the same to a media query as one stretching across a full viewport at the same screen size, forcing developers to maintain increasingly specific breakpoints to accommodate every layout scenario.

Container queries change that mindset entirely. Instead of keying styles off the viewport, they let you respond to an element's actual context — its dimensions or even its computed styles. The feature comes in two flavors: container size queries for tracking dimensions and container style queries for tracking style property values. Both are defined in the CSS Containment Module Level 3 specification, though support is uneven: size queries enjoy roughly 90% global browser support, while style queries are still experimental, available behind feature flags in Chrome 111+ and Safari Technology Preview.

Understanding what makes style queries valuable requires first understanding what a "container" is and how containment works.

The Containment Model

A container is simply any ancestor element — parent, grandparent, or further up the tree — that has a containment context. There are two types of containment contexts, distinguished by what you want to track. A size containment context lets you query and track an element's dimensions including width, height, inline-size, block-size, aspect-ratio, and orientation. A style containment context lets you query and track an element's computed style values, such as custom properties or, eventually, properties like background-color and display.

The critical difference is how these contexts are established. Registering size containment is opt-in because continuously tracking dimensions of every element would be a massive performance burden — the browser would have to monitor resizing, scrolling, and animations across the entire DOM. That's why you must explicitly declare a size containment using the container-type property when you want to query dimensions. Style containment, by contrast, is automatic: every element is a style container by default. Checking raw style properties before the browser processes them is cheap enough that no opt-in is required.

The container-type property accepts three values that determine what kind of context an element gets:

/* Size containment in the inline direction */
.parent {
  container-type: inline-size;
}

Notably, normal — which establishes style containment — is rarely written explicitly because it is already the default state for all elements. You might see it used to override a size containment set elsewhere, though the initial or revert keywords can serve the same purpose.

Size Queries: The Established Feature

When talking about inline-size, we're referring to the inline direction of an element given its current writing mode. In horizontal writing modes, that corresponds to width; rotate the writing mode to vertical and inline maps to height instead. Establishing a size containment context on a container like .cards-container allows descendant styles to react when the container's inline dimension gets too cramped:

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

  @container (width < 700px) {
  .cards {
    background-color: red;
  }
}

The syntax feels familiar if you've written media queries before. The same logical operators — and, or, and not — work for chaining multiple conditions. Type a size query looks for the nearest ancestor with a size containment context; if none exists, the @container rule is ignored. Because matching the "nearest" element can also catch unintended ancestors, the container-name property is useful for explicitly naming containers you want to target, and the container shorthand sets name and type together:

.cards-container {
  container: cardsContainer / inline-size;

  /* Equivalent to: */
  container-name: cardsContainer;
  container-type: inline-size;
}

Setting container-type to size rather than inline-size broadens the containment context to cover both inline and block directions, allowing you to query height as well as width — at the cost of forcing the container to manage its block-size and apply layout containment to itself.

When the CSS Cascade evaluates multiple non-chained container rules that match, the most specific selector wins, consistent with standard cascade behavior.

Style Queries: What Makes Them Different

Where size queries solve the problem of cramped components, style queries address something subtler: component styling driven by state or context rather than dimensions. The idea is straightforward. Define a custom property on a container:

.cards-container {
  --theme: dark;
}

Then check whether a container carries that property value and apply descendant styles conditionally, without affecting the container itself — directly styling the container could spark an infinite loop of style changes triggering new queries. The style() wrapper differentiates these tests from size queries:

/* Size query */
@container (width > 60ch) {
  .cards {
    flex-direction: column;
  }
}

/* Style query */
@container style(--theme: dark) {
  .cards {
    background-color: black;
  }
}

Why a wrapper function at all? Look ahead to what's being proposed: a future version of this feature might allow checking whether a container has computed values such as max-width: 400px as a style query instead of, or alongside, the geometric reasoning of a size query. The style() function makes the intent unambiguous.

Just as with size queries, the browser finds the closest ancestor with a matching containment context. In style queries' case, that is always an element's direct parent since every element already has style containment by default. Want to query a grandparent or another non-direct ancestor? The container-name property must be involved to disambiguate:

.cards-container {
  container-name: cardsContainer;
  --theme: dark;
}

@container cardsContainer style(--theme: dark) {
  .card {
    color: white;
  }
}

The practical scenarios this unlocks are easy to imagine. A card component could inherit a --theme custom property from its parent and render with appropriate text and accent colors automatically. A pagination control could adjust its visual density based on an embedded color scheme indicator. The common thread is that appearance decisions depend on context that was previously difficult or impossible for CSS to express without JavaScript gymnastics.

All of it hinges on the containment context that style queries use — a paradigm shift from "what size is the screen?" to "what is the state of this component's environment?" As experimental as style queries remain today, that shift is the more profound half of what CSS Container Queries offer to a web that increasingly builds ambitious designs from self-contained, reusable components.

Style Queries and Containment: The Fine Print

Container style queries are a genuinely new capability in CSS, so it’s no surprise they come with some quirky and occasionally counterintuitive behaviors. Some of these are deliberate design decisions; others feel like side effects that the spec may need to revisit down the road.

Size and Style Containment Can Coexist

You might assume that setting container-type: inline-size on an element would disable style queries on it, since size and style containment sound like mutually exclusive concerns. In practice, that’s not the case. All elements have style containment by default, and there is no way to remove it. Even when you explicitly declare a size container type, you can still query that element for style conditions.

.cards-container {
  container-type: inline-size;
  --theme: dark;
}

@container style(--theme: dark) {
  .card {
    background-color: black;
  }
}

@container (width < 700px) {
  .card {
    background-color: red;
  }
}

That seems contradictory until you realize that size and style queries are evaluated independently. There’s no inherent conflict: a style query might adjust a color based on a custom property, while a size query changes flex-direction when the container gets too tight. When both types of queries try to apply conflicting styles, the cascade resolves it — normally the selector with higher specificity wins, as in the example where the size query overrides the background at narrower widths.

See the Pen [Conflicting Style and Size Queries [forked]](https://codepen.io/smashingmag/pen/KKLyeQQ) by Monknow.

See the Pen Conflicting Style and Size Queries [forked] by Monknow.

Unnamed Queries Search the Whole Ancestor Chain

Another oddity: an unnamed style query does not stop at the direct parent. In the example above, .card is inside a .cards parent that has no --theme property at all. Yet the style query still matches, because it walks up the DOM until it finds a custom property it can test against. The property is defined on .cards-container, which is further up the tree.

.cards-container {
--theme: dark;  
}

@container style(--theme: dark) {
  /* This is still active! */
  .card {
    background-color: black;
  }
}

If you add a different --theme value to the intermediate .cards element, the query begins using that element as its container and stops matching. This behavior seems only half-intentional; it demonstrates how much care you need when relying on unnamed containers for style queries.

.cards-container {
--theme: dark;  
}

.cards {
  --theme: light; /* This is now the matching container for the query */
}

/* This query is no longer active! */
@container style(--theme: dark) {
  .card {
    background-color: black;
  }
}

Style Is the Default Container Type, But Not the Default Query

There’s also a mismatch in how the syntax works. Style queries are the default container type (every element is a style container), yet to actually query styles you must explicitly write the style() function. There is no equivalent size() keyword — size queries are just written without a wrapper function. It’s not a flaw in the spec, just something you have to keep in mind while writing code.

Practical Value: A Closer Look

On the surface, style queries seem full of promise. But once you start building real examples, it’s hard to pin down a problem they solve that doesn’t already have a simpler, well-trodden solution. The new tool is liberating in the abstract, but managing state and styles can get complicated quickly, especially when you’re already struggling to name things.

The Obvious Demo: Themed Leaderboard Items

Since style queries can presently only test custom properties, the clearest use case is holding a bit of state in a custom property and changing UI styles when that value changes. Imagine a web app with a global leaderboard. First place gets a gold background, second silver, third bronze, and everyone else gets the same default styling. The data is updated server-side, so you can inject each player’s rank directly into the markup as an inline custom property, --position: number.

<ol>
  <li class="item-container" style="--position: 1">
    <div class="item">
      <img src="..." alt="Roi's avatar" />
      <h2>Roi</h2>
    </div>
  </li>
  <li class="item-container" style="--position: 2"><!-- etc. --></li>
  <li class="item-container" style="--position: 3"><!-- etc. --></li>
  <li class="item-container" style="--position: 4"><!-- etc. --></li>
  <li class="item-container" style="--position: 5"><!-- etc. --></li>
</ol>

Style queries then let you target each rank without additional class names or attributes.

.item-container {
  container-name: leaderboard;
  /* No need to apply container-type: normal */
}

@container leaderboard style(--position: 1) {
  .item {
    background: linear-gradient(45deg, yellow, orange); /* gold */
  }
}

@container leaderboard style(--position: 2) {
  .item {
    background: linear-gradient(45deg, grey, white); /* silver */
  }
}

@container leaderboard style(--position: 3) {
  .item {
    background: linear-gradient(45deg, brown, peru); /* bronze */
  }
}

See the Pen [Style Queries Use Case [forked]](https://codepen.io/smashingmag/pen/vYwWrRL) by Monknow.

See the Pen Style Queries Use Case [forked] by Monknow.

If your browser doesn’t fully support style queries in the embedded demo, opening it in a separate tab should work. A screenshot is also included below for reference.

Two leaderboard UIs. One before style queries and one after styles have been applied
Figure 2: We can apply styles to an element’s children and descendants when the element’s styles match a certain condition. (Large preview)

Classes and IDs Do The Same Job With Less Code

Almost every example of style queries I’ve seen follows that pattern, and my immediate reaction is that a class or ID already solves the problem more cleanly. Rather than passing state through an inline custom property, you can just set a class on the element based on its rank.

<ol>
  <li class="item first">
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>
  <li class="item second"><!-- etc. --></li>
  <li class="item third"><!-- etc. --></li>
  <li class="item"><!-- etc. --></li>
  <li class="item"><!-- etc. --></li>
</ol>

You could even encode the rank in the ID, avoiding the need to convert a number into a string:

<ol>
  <li class="item" id="item-1">
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>
  <li class="item" id="item-2"><!-- etc. --></li>
  <li class="item" id="item-3"><!-- etc. --></li>
  <li class="item" id="item-4"><!-- etc. --></li>
  <li class="item" id="item-5"><!-- etc. --></li>
</ol>

Both of those approaches leave your HTML cleaner than the container query version, which forces you to wrap elements in a container even when the structure doesn’t need one — that’s because containers can’t style themselves. The CSS ends up less verbose too:

#item-1 {
  background: linear-gradient(45deg, yellow, orange); 
}

#item-2 {
  background: linear-gradient(45deg, grey, white);
}

#item-3 {
  background: linear-gradient(45deg, brown, peru);
}

See the Pen [Style Queries Use Case Replaced with Classes [forked]](https://codepen.io/smashingmag/pen/oNRoydN) by Monknow.

See the Pen Style Queries Use Case Replaced with Classes [forked] by Monknow.

Using IDs as styling hooks is normally discouraged, but only because they must be unique on a page. In this leaderboard, there will never be more than one item at first, second, or third place, so IDs are safe. You could equally reach for a data-* attribute if that feels better.

What Would Make Style Queries Compelling

The one thing that could substantially raise the value of style queries is a range syntax for querying custom property values — a featureMiriam Suzanne proposed in 2023. It would let you compare numerical values similar to what size queries allow.

Consider giving the rest of the top ten a distinct background. Instead of writing a style query for each position from four through ten, you could express the whole range in one query. The syntax isn’t in any spec yet, but for illustration, imagine something close to this:

/* Do not try this at home! */
@container leaderboard style(4 >= --position <= 10) {
  .item {
    background: linear-gradient(45deg, purple, fuchsia);
  }
}

That hypothetical check would:

  • Target a container named leaderboard,
  • Run a style() query on it,
  • Test the --position custom property,
  • Trigger when the value is at least 4 and no more than 10,
  • And apply a linear-gradient() from purple to fuschia.

That would be nice, but in a React or Vue component you can accomplish the same thing with a small range check in JavaScript and a conditional class like .top-ten.

See the Pen [Style Ranged Queries Use Case Replaced with Classes [forked]](https://codepen.io/smashingmag/pen/OJYOEZp) by Monknow.

See the Pen Style Ranged Queries Use Case Replaced with Classes [forked] by Monknow.

Moving Style Logic Out of Components

The leaderboard examples don’t make style queries look essential, but that doesn’t mean they’re worthless. The strongest argument in their favor is separating style-related logic from the rest of your application logic. In a React component, per-player background selection means writing conditional statements like:

const LeaderboardItem = ({position}) => {
  <li className={`item ${position >= 4 && position <= 10 ? "top-ten" : ""}`} id={`item-${position}`}>
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>;
};

That’s messy JSX, and it drags presentational concerns into component logic. Style queries let you instead pass the raw --position value to the stylesheet and handle all the conditional styling directly in CSS.

const LeaderboardItem = ({position}) => {
  <li className="item" style={{"--position": position}}>
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>;
};

That’s a much cleaner separation of concerns and feels closer to the real value of style queries. But it depends on a range syntax being added to the spec — which isn’t guaranteed.

Final Thoughts

Modern CSS is improving from the work of many teams, and not every feature has to be a revolutionary shift. Still, style queries currently feel more like a solution searching for a problem — unlike size queries, which are a clear upgrade over media queries for responsive design. They don’t fix a pressing issue, and they’re not cleanly better than existing solutions, at least as far as I can tell.

If style queries eventually learn to check any CSS property, not just custom properties, that would open a very different, and arguably chaotic, door: styles reacting to other styles in an unbounded chain with extra boilerplate. Writing styles declaratively in one place still seems like the more sane approach.

Maybe there are strong use cases in browser extensions, such as tools like Dark Reader that need to inspect styles on third-party sites. I don’t see it clearly yet. If you’ve found better uses for style queries, I’d be glad to hear about them in the comments and learn how you think about working with this new tool.

Smashing Editorial