The Range Syntax Extends CSS Conditional Logic
The range syntax has been available for media queries and container size queries for a while, letting authors compare viewport and container dimensions against thresholds. Now, starting with Chrome 142, that same syntax works with container style queries and the if() function. This opens up comparisons against literal numeric values, as well as values coming from custom properties or the attr() function.
Here's what range comparisons look like in both contexts when testing a custom property (--lightness) against a fixed value (50%):
#container {
/* Choose any value 0-100% */
--lightness: 10%;
/* Applies it to the background */
background: hsl(270 100% var(--lightness));
color: if(
/* If --lightness is less than 50%, white text */
style(--lightness < 50%): white;
/* If --lightness is more than or equal to 50%, black text */
style(--lightness >= 50%): black
);
/* Selects the children */
* {
/* Specifically queries parents */
@container style(--lightness < 50%) {
color: white;
}
@container style(--lightness >= 50%) {
color: black;
}
}
}
You'll need Chrome 142 or later to view the demos in this article.
Custom Properties in Style Queries
Container style queries can't inspect ordinary CSS properties directly. The workaround is to store the value you care about in a custom property. In the demo below, --lightness is defined on #container and used to build the background color via HSL.
#container {
/* Choose any value 0-100% */
--lightness: 10%;
/* Applies it to the background */
background: hsl(270 100% var(--lightness));
}
Child elements then use conditional color declarations based on that custom property. When --lightness is below 50%, the text becomes white; at 50% or above, it switches to black.
#container {
/* etc. */
/* Selects the children */
* {
/* Specifically queries parents */
@container style(--lightness < 50%) {
color: white;
}
@container style(--lightness >= 50%) {
color: black;
}
}
}
Those @container rules must live on the children, not on #container itself. Placing them there would make the query look for --lightness on #container's own parent, where the property doesn't exist.
Before the range syntax landed, style queries could only match exact values. The new syntax makes them far more practical for threshold-based styling.
The equivalent if()-based approach is more flexible about placement; it works whether you put the declaration on the container or on its children:
#container {
--lightness: 10%;
background: hsl(270 100% var(--lightness));
/* --lightness works here */
color: if(
style(--lightness < 50%): white;
style(--lightness >= 50%): black
);
* {
/* And here! */
color: if(
style(--lightness < 50%): white;
style(--lightness >= 50%): black
);
}
}
So why reach for container style queries when if() can handle similar logic? The key advantage is scoping. Container queries let you anchor the search to a specific containment context via the container-name property:
#container {
--lightness: 10%;
background: hsl(270 100% var(--lightness));
/* Define a named containment context */
container-name: myContainer;
* {
/* Specify the name here */
@container myContainer style(--lightness < 50%) {
color: white;
}
@container myContainer style(--lightness >= 50%) {
color: black;
}
}
}
With container-name: myContainer, the @container rule only fires when it finds --lightness on that named container. If the property isn't there, the block is skipped entirely. You can also declare the same container-name further up the cascade to expand the search scope. The if() function gives you no such control; container queries let you dictate exactly where the lookup happens.
Pulling Numbers from HTML Attributes
The range syntax also works with values pulled from HTML attributes via attr(). Consider a badge showing unread notification count, supplied through a data-notifs attribute:
<div></div>
The goal is to render that number inside [data-notifs] using a ::after pseudo-element. The @container rules go on that pseudo-element, with [data-notifs] itself declared as the container. Some basic height and border-radius styling sets up the badge shape:
[data-notifs]::after {
height: 1.25rem;
border-radius: 1.25rem;
/* Container style queries here */
}
The first query handles counts up to 99 (one or two digits). When it matches, content: attr(data-notifs) inserts the raw number and aspect-ratio: 1 / 1 keeps the badge circular. The second query catches counts greater than 99 and swaps in the literal string "99+", since a circular badge can't comfortably fit four digits. Inline padding replaces a fixed width so the wider text still fits.
[data-notifs]::after {
height: 1.25rem;
border-radius: 1.25rem;
/* If notification count is 1-2 digits */
@container style(attr(data-notifs type(<number>)) <= 99) {
/* Display count */
content: attr(data-notifs);
/* Make width equal the height */
aspect-ratio: 1 / 1;
}
/* If notification count is 3 or more digits */
@container style(attr(data-notifs type(<number>)) > 99) {
/* After 99, simply say "99+" */
content: "99+";
/* Instead of width, a little padding */
padding-inline: 0.1875rem;
}
}
Notice the query uses attr(data-notifs type(<number>)) rather than a bare attr(data-notifs). Without an explicit type, attr() resolves to a string. That's fine for outputting the value via content, but comparing it against a numeric 99 requires the value to be typed as a number (an integer type would work here too).
All comparatives in a range expression must share the same data type, though units may differ. Supported types include <length>, <number>, <percentage>, <angle>, <time>, <frequency>, and <resolution>. The earlier lightness demo could even drop its % unit, since modern hsl() accepts unitless values — as long as every side of the comparison stays unitless too:
#container {
/* 10, not 10% */
--lightness: 10;
background: hsl(270 100 var(--lightness));
color: if(
/* 50, not 50% */
style(--lightness < 50): white;
style(--lightness >= 50): black
);
* {
/* 50, not 50% */
@container style(--lightness < 50) {
color: white;
}
@container style(--lightness >= 50) {
color: black;
}
}
}
This notification pattern doesn't map cleanly onto if(), because each property would require its own conditional logic. It's technically possible, but style queries keep the logic consolidated.
Comparing Literal Lengths
Range comparisons also work between literal values — for instance, checking whether 1em is smaller than 32px. These are different units but the same data type (<length>), which is all that's required.
Consider an <h1> with a font-size of 31px. A <span> inside it inherits that size, making 1em within the span equal to 31px. Using if(), we can adjust the font-weight accordingly: when 1em is less than 32px, use a light 100; when it's 32px or larger, go heavy with 900.
Remove the explicit font-size, and 1em falls back to the user agent default of 32px. In that case neither branch matches, so the font-weight also reverts to its default (700 for headings). This approach keeps text readable at small sizes and prevents overly bold large text, or thin text that's too small to read comfortably.
<h1>
<span>Heading 1</span>
</h1>
h1 {
/*
The default value is 32px,
but we overwrite it to 31px,
causing the first if() condition to match
*/
font-size: 31px;
span {
/* Here, 1em is equal to 31px */
font-weight: if(
style(1em < 32px): 100;
style(1em > 32px): 900
);
}
}
The addition of range syntax to style queries and if() marks a significant step forward for conditional logic in CSS. Combined with media queries, feature queries, and other container query types (remember to set container-type when mixing with size queries), the expressive power available in pure CSS continues to grow. For more background, these guides cover the broader query landscape:



