The Range Syntax Has Come to Container Style Queries and if()
Publikováno: 13.11.2025
Being able to use the range syntax with container style queries — which we can do starting with Chrome 142 — means that we can compare literal numeric values as well as numeric values tokenized by custom properties or the attr() function.
The Range Syntax Has Come to Container Style Queries and if() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The range syntax isn’t a new thing. We‘re already able to use it with media queries to query viewport dimensions and resolutions, as well as container size queries to query container dimensions. Being able to use it with container style queries — which we can do starting with Chrome 142 — means that we can compare literal numeric values as well as numeric values tokenized by custom properties or the attr() function.
In addition, this feature comes to the if() function as well.
Here’s a quick demo that shows the range syntax being used in both contexts to compare a custom property (--lightness) to a literal 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;
}
}
}Again, you’ll want Chrome 142 or higher to see this work:
Both methods do the same thing but in slightly different ways.
Let’s take a closer look.
Range syntax with custom properties
In the next demo coming up, I’ve cut out the if() stuff, leaving only the container style queries. What’s happening here is that we’ve created a custom property called --lightness on the #container. Querying the value of an ordinary property isn’t possible, so instead we save it (or a part of it) as a custom property, and then use it to form the HSL-formatted value of the background.
#container {
/* Choose any value 0-100% */
--lightness: 10%;
/* Applies it to the background */
background: hsl(270 100% var(--lightness));
}After that we select the container’s children and conditionally declare their color using container style queries. Specifically, if the --lightness property of #container (and, by extension, the background) is less than 50%, we set the color to white. Or, if it’s more than or equal to 50%, we set the color to black.
#container {
/* etc. */
/* Selects the children */
* {
/* Specifically queries parents */
@container style(--lightness < 50%) {
color: white;
}
@container style(--lightness >= 50%) {
color: black;
}
}
}/explanation Note that we wouldn’t be able to move the @container at-rules to the #container block, because then we’d be querying --lightness on the container of #container (where it doesn’t exist) and then beyond (where it also doesn’t exist).
Prior to the range syntax coming to container style queries, we could only query specific values, so the range syntax makes container style queries much more useful.
By contrast, the if()-based declaration would work in either block:
#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, given that container style queries only look up the cascade (whereas if() also looks for custom properties declared within the same CSS rule) why use container style queries at all? Well, personal preference aside, container queries allow us to define a specific containment context using the container-name CSS 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 this version, if the @container at-rule can’t find --lightness on myContainer, the block doesn’t run. If we wanted @container to look further up the cascade, we’d only need to declare container-name: myContainer further up the cascade. The if() function doesn’t allow for this, but container queries allow us to control the scope.
Range syntax with the attr() CSS function
We can also pull values from HTML attributes using the attr() CSS function.
In the HTML below, I’ve created an element with a data attribute called data-notifs whose value represents the number of unread notifications that a user has:
<div data-notifs="8"></div>We want to select [data-notifs]::after so that we can place the number inside [data-notifs] using the content CSS property. In turn, this is where we’ll put the @container at-rules, with [data-notifs] serving as the container. I’ve also included a height and matching border-radius for styling:
[data-notifs]::after {
height: 1.25rem;
border-radius: 1.25rem;
/* Container style queries here */
}Now for the container style query logic. In the first one, it’s fairly obvious that if the notification count is 1-2 digits (or, as it’s expressed in the query, less than or equal to 99), then content: attr(data-notifs) inserts the number from the data-notifs attribute while aspect-ratio: 1 / 1 ensures that the width matches the height, forming a circular notification badge.
In the second query, which matches if the number is more than 99, we switch to content: "99+" because I don’t think that a notification badge could handle four digits. We also include some inline padding instead of a width, since not even three characters can fit into the circle.
To summarize, we’re basically using this container style query logic to determine both content and style, which is really cool:
[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;
}
}But you’re likely wondering why, when we read the value in the container style queries, it’s written as attr(data-notifs type(<number>) instead of attr(data-notifs). Well, the reason is that when we don’t specify a data type (or unit, you can read all about the recent changes to attr() here), the value is parsed as a string. This is fine when we’re outputting the value with content: attr(data-notifs), but when we’re comparing it to 99, we must parse it as a number (although type(<integer>) would also work).
In fact, all range syntax comparatives must be of the same data type (although they don’t have to use the same units). Supported data types include <length>, <number>, <percentage>, <angle>, <time>, <frequency>, and <resolution>. In the earlier example, we could actually express the lightness without units since the modern hsl() syntax supports that, but we’d have to be consistent with it and ensure that all comparatives are unit-less 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;
}
}
}Note: This notification count example doesn’t lend itself well to if(), as you’d need to include the logic for every relevant CSS property, but it is possible and would use the same logic.
Range syntax with literal values
We can also compare literal values, for example, 1em to 32px. Yes, they’re different units, but remember, they only have to be the same data type and these are both valid CSS <length>s.
In the next example, we set the font-size of the <h1> element to 31px. The <span> inherits this font-size, and since 1em is equal to the font-size of the parent, 1em in the scope of <span> is also 31px. With me so far?
According to the if() logic, if 1em is equal to less than 32px, the font-weight is smaller (to be exaggerative, let’s say 100), whereas if 1em is equal to or greater than 32px, we set the font-weight to a chunky 900. If we remove the font-size declaration, then 1em computes to the user agent default of 32px, and neither condition matches, leaving the font-weight to also compute to the user agent default, which for all headings is 700.
Basically, the idea is that if we mess with the default font-size of the <h1>, then we declare an optimized font-weight to maintain readability, preventing small-fat and large-thin text.
<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
);
}
}CSS queries have come a long way, haven’t they?
In my opinion, the range syntax coming to container style queries and the if() function represents CSS’s biggest leap in terms of conditional logic, especially considering that it can be combined with media queries, feature queries, and other types of container queries (remember to declare container-type if combining with container size queries). In fact, now would be a great time to freshen up on queries, so as a little parting gift, here are some links for further reading:
The Range Syntax Has Come to Container Style Queries and if() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.