Container Query Range Syntax and Logic Operators

Most container queries in the wild are a single min-width test. That covers the simple case — switch a card to a row layout above some width — but component layouts often need more precise conditions: a middle band between two widths, a state that applies when a container is either very narrow or very short, a rule for everything except a range. Container queries support a full condition language with range comparisons and and, or and not, and writing those conditions correctly avoids both overlapping breakpoints and the silent failures that come from ambiguous logic. This page covers the syntax, the precedence rules, and the queries that only work on size containers. It belongs to Container Query Syntax Basics in the Mastering Container Queries & Responsive Layouts guide.

Why range syntax matters

The classic min-width/max-width pairs have a boundary problem. With (max-width: 30rem) for the narrow layout and (min-width: 30rem) for the wide one, a container exactly 30rem wide matches both, and whichever rule comes later wins. Off-by-one-pixel fixes such as max-width: 29.99rem paper over it but leave a gap at fractional widths. Range syntax states the boundary exactly: width < 30rem and width >= 30rem share the breakpoint with no overlap and no gap.

Range syntax is also easier to read. (20rem <= width < 40rem) says "between 20 and 40" in one expression, where the legacy form needs (min-width: 20rem) and (max-width: 39.99rem). For container queries, which are newer than media queries, there is no compatibility reason to prefer the old form: every engine that supports @container supports range comparisons.

Overlap at the breakpoint versus a clean boundary Top: max-width 30rem and min-width 30rem both include the value 30rem, so a container exactly that wide matches both. Bottom: width less than 30rem and width greater than or equal to 30rem meet at the boundary without overlap. What happens at exactly 30rem 30rem max-width: 30rem min-width: 30rem both match at 30rem: source order decides width < 30rem width >= 30rem exactly one matches at every width

The complete implementation

The component below has three bands — compact, regular and wide — plus a rule for short containers and a rule that applies outside the regular band. Drag the demo to see each condition switch.

Live demoExclusive width bands plus or and not conditions
Resize the dashed panel in both directions: the label names the active band, a short or narrow panel shrinks its text, and bands outside the middle get an outline. Drag the bottom-right corner to resize the frame in either direction.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Container query ranges and logic</title>
<style>
  body { font: 15px/1.5 system-ui, sans-serif; margin: 1rem; }

  .panel-host {
    /* size containment in both axes so height conditions can match */
    container: panel / size;
    height: 14rem;
    resize: both;
    overflow: auto;
    border: 1px dashed #94a3b8;
  }

  .panel { display: grid; gap: 0.5rem; padding: 1rem; height: 100%; box-sizing: border-box; }
  .panel__label::after { content: " · base"; }

  /* Three exclusive bands with no overlap and no gap. */
  @container panel (width < 20rem) {
    .panel__label::after { content: " · compact"; }
  }
  @container panel (20rem <= width < 40rem) {
    .panel { grid-template-columns: 1fr 1fr; }
    .panel__label::after { content: " · regular"; }
  }
  @container panel (width >= 40rem) {
    .panel { grid-template-columns: repeat(3, 1fr); }
    .panel__label::after { content: " · wide"; }
  }

  /* OR: tight in either axis. Parentheses around each test are required. */
  @container panel (width < 16rem) or (height < 10rem) {
    .panel { font-size: 0.85rem; }
  }

  /* NOT: everything outside the regular band gets a border accent. */
  @container panel not (20rem <= width < 40rem) {
    .panel { outline: 2px solid #6366f1; outline-offset: -2px; }
  }

  /* AND with a nested OR: parentheses make the grouping explicit. */
  @container panel (width >= 30rem) and ((orientation: landscape) or (aspect-ratio > 2)) {
    .panel__hint { display: block; }
  }
  .panel__hint { display: none; color: #475569; }
</style>
</head>
<body>
  <div class="panel-host">
    <div class="panel">
      <strong class="panel__label">Layout</strong>
      <div>One</div><div>Two</div><div>Three</div>
      <p class="panel__hint">Wide and landscape: extra hint shown.</p>
    </div>
  </div>
</body>
</html>

The host uses container: panel / size, the shorthand for a name and a type. Size containment in both axes is what allows the height, orientation and aspect-ratio conditions to match; on an inline-size container those tests never match because the container has no definite block size to compare.

The key technique: parentheses define the grouping

The condition language deliberately forbids mixing and and or at the same level, so there is never a question of which binds tighter. An expression like (a) and (b) or (c) is invalid, and an invalid condition makes the entire @container rule never match — silently.

Grouping rules for container conditions Invalid: (a) and (b) or (c). Valid: (a) and ((b) or (c)), ((a) and (b)) or (c), and not (a). The not keyword applies to one parenthesised condition and cannot be chained with and or or without its own parentheses. Mixing and/or needs explicit parentheses (a) and (b) or (c) invalid: rule never applies (a) and ((b) or (c)) valid: a, plus b or c ((a) and (b)) or (c) valid: both a and b, or c not ((a) or (b)) valid: neither a nor b

not negates exactly one condition in parentheses, so not (a) and (b) is also invalid; write (not (a)) and (b) or restructure. When a container query mysteriously never applies, an ungrouped mix of operators is one of the first things to check — DevTools will show the rule, but it will never be marked as matching.

Choosing where the bands begin

Range syntax makes band boundaries precise, which raises the question of where they should be. Viewport breakpoints are usually chosen from device classes; component bands should be chosen from the component's content.

Start from the smallest width at which each layout still works. For a card that shows an image beside text, that is the image's minimum comfortable width plus the gap plus the shortest line of text that reads well — perhaps 8rem, 1rem and 12rem, giving a threshold of 21rem. Below it, the side-by-side layout squeezes text into a column too narrow to read, so the band boundary belongs there, not at a round number. Deriving thresholds this way also makes them explainable in a code review, which arbitrary round numbers are not.

Prefer rem over px for thresholds. A container measured in rem scales with the user's font size, so a reader who has set a larger default font sees the compact layout earlier, which is exactly right: their text needs more room. Pixel thresholds ignore that setting and can leave enlarged text squeezed into a layout designed for smaller type.

Keep the number of bands small. Two or three bands per component — compact, regular and occasionally wide — are usually enough, because flexbox wrapping, grid auto-fit and fluid values handle the continuous adjustments between them. A component with six bands is often a component whose internal layout should be more intrinsic.

Which features a container can be queried on

Size container queries accept the size features: width, height, inline-size, block-size, aspect-ratio and orientation, each with range syntax. inline-size and block-size are the logical forms, which follow the writing mode and are the better choice for components that may appear in vertical text. Which of these can match depends on the container type:

  • container-type: inline-size establishes containment in the inline axis only. width/inline-size conditions work; height, block-size, aspect-ratio and orientation never match.
  • container-type: size establishes containment in both axes, so every size feature works — but the container no longer sizes its height from its content, so it needs an explicit height. The trade-off is explained in container-type: size vs inline-size.

Style queries use a separate function, style(), and can be combined with size conditions in the same @container rule using the same and, or and not operators — a pattern developed in Combining Size and Style Queries.

Variation: named queries and custom breakpoints

Range conditions become easier to maintain when their thresholds are shared. Custom properties cannot be used inside container conditions — (width > var(--bp)) is invalid, because conditions are evaluated before custom properties resolve — so shared breakpoints are usually kept in a preprocessor or documented as a small set of standard values.

/* Documented component bands, used consistently across the library:
   compact  < 20rem
   regular  20rem – 40rem
   wide     >= 40rem */
@container card (width < 20rem)            { .card { --pad: 0.75rem; } }
@container card (20rem <= width < 40rem)   { .card { --pad: 1rem; } }
@container card (width >= 40rem)           { .card { --pad: 1.5rem; } }

/* The bands set tokens; the component reads them once. */
.card { padding: var(--pad, 1rem); }

Setting custom properties inside the bands, and reading them in a single component rule, keeps the conditions short and the component's layout logic in one place. It also makes the bands easy to animate across: a registered --pad can transition when the container crosses a boundary, which is the idea behind container-query-triggered keyframe animations.

Browser support

Container size queries with range syntax and and, or and not are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. Media query range syntax, the same notation in @media, is supported in Chrome and Edge 104+, Firefox 102+ and Safari 16.4+. Style queries for custom properties, combinable with these conditions, are supported in Chrome and Edge 111+, Firefox 151+ and Safari 18+.

FAQ

Can container queries use range syntax like width > 30rem? Yes. Container size queries were specified with range syntax from the start, so @container (width > 30rem) and @container (20rem <= width < 40rem) both work in every engine that supports container queries. The older min-width and max-width forms also work.

Why is my container query with and and or invalid? CSS does not allow and and or to be mixed at the same level without parentheses. Write (a) and ((b) or (c)) rather than (a) and (b) or (c). The whole condition is dropped if it is ambiguous.

Is min-width: 30rem the same as width >= 30rem? Yes. min-width is inclusive, so it matches at exactly 30rem, which is the same as width >= 30rem. The range form makes exclusive boundaries such as width > 30rem possible, which avoids both of two adjacent queries matching at the exact breakpoint.

Can a container query test orientation or aspect ratio? Yes, but only on a container with size containment in both axes, which means container-type: size. An inline-size container has no definite block size to compare, so orientation and aspect-ratio queries against it never match.

Related articles

More pages in the same section.