Sometimes I want to set the value of a CSS property to that of a different property, even if I don’t know what that value is, and even if it changes later. Unfortunately though, that’s not possible (at least, there isn’t a CSS function that specifically does that).
In my opinion, it’d be super useful to have something like this (for interpolation, maybe you’d throw calc-size() in there as well):
In 2021, Lea Verou explained why, despite being proposed numerous times, implementing such a general-purpose CSS function like this isn’t feasible. Having said that, I do remain hopeful, because things are always evolving and the CSSWG process isn’t always linear.
In the meantime, even though there isn’t a CSS function that enables us to get the value of a different property, you might be able to achieve your outcome using a different method, and those methods are what we’re going to look at today.
The fool-proof CSS custom properties method
We can easily get the value of a different CSS property using custom properties, but we’d need to know what the value is in order to declare the custom property to begin with. This isn’t ideal, but it does enable us to achieve some outcomes.
Let’s jump back to the example from the intro where we try to set the border-radius based on the height, only this time we know what the height is and we store it as a CSS custom property for reusability, and so we’re able to achieve our outcome:
We can even place that --button-height custom property higher up in the CSS cascade to make it available to more containment contexts.
:root {
/* Declare here to use anywhere */
--button-height: 3rem;
header {
--header-padding: 1rem;
padding: var(--header-padding);
/* Height is unknown (but we can calculate it) */
--header-height: calc(var(--button-height) + (var(--header-padding) * 2));
/* Which means we can calculate this, too */
border-radius: calc(var(--header-height) * 0.3);
button {
/* As well as these, of course */
height: var(--button-height);
border-radius: calc(var(--button-height) * 0.3);
/* Oh, what the heck */
padding-inline: calc(var(--button-height) * 0.5);
}
}
}
I guess when my math teacher said that I’d need algebra one day. She wasn’t lying!
The unsupported inherit() CSS function method
The inherit() CSS function, which isn’t currently supported by any web browser, will enable us to get the value of a parent’s property. Think: the inherit keyword, except that we can get the value of any parent property and even modify it using value functions such as calc(). The latest draft of the CSS Values and Units Module Level 5 spec defines how this’d work for custom properties, which wouldn’t really enable us to do anything that we can’t already do (as demonstrated in the previous example), but the hope is that it’d work for all CSS properties further down the line so that we wouldn’t need to use custom properties (which is just a tad longer):
header {
height: 3rem;
button {
height: 100%;
/* Get height of parent but use it here */
border-radius: calc(inherit(height) * 0.3);
padding-inline: calc(inherit(height) * 0.5);
}
}
There is one difference between this and the custom properties approach, though. This method depends on the fixed height of the parent, whereas with the custom properties method either the parent or the child can have the fixed height.
This means that inherit() wouldn’t interpolate values. For example, an auto value that computes to 3rem would still be inherited as auto, which might compute to something else when inherit()-ed., Sometimes that’d be fine, but other times it’d be an issue. Personally, I’m hoping that interpolation becomes a possibility at some point, making it far more useful than the custom properties method.
Until then, there are some other (mostly property-specific) options.
The aspect-ratio CSS property
Using the aspect-ratio CSS property, we can set the height relative to the width, and vice-versa. For example:
div {
width: 30rem;
/* height will be half of the width */
aspect-ratio: 2 / 1;
/* Same thing */
aspect-ratio: 3 / 1.5;
/* Same thing */
aspect-ratio: 10 / 5;
/* width and height will be the same */
aspect-ratio: 1 / 1;
}
Technically we don’t “get” the width or the height, but we do get to set one based on the other, which is the important thing (and since it’s a ratio, you don’t need to know the actual value — or unit — of either).
The currentColor CSS keyword
The currentColor CSS keyword resolves to the computed value of the color property. Its data type is <color>, so we can use it in place of any <color> on any property on the same element. For example, if we set the color to red (or something that resolves to red), or if the color is computed as red via inheritance, we could then declare border-color: currentColor to make the border red too:
body {
/* We can set color here (and let it be inherited) */
color: red;
button {
/* Or set it here */
color: red;
/* And then use currentColor here */
border-color: currentColor;
border: 0.0625rem solid currentColor;
background: hsl(from currentColor h s 90);
}
}
This enables us to reuse the color without having to set up custom properties, and of course if the value of color changes, currentColor will automatically update to match it.
While this isn’t the same thing as being able to get the color of literally anything, it’s still pretty useful. Actually, if something akin to compute(background-color) just isn’t possible, I’d be happy with more CSS keywords like currentColor.
In fact, currentBackgroundColor/currentBackground has already been proposed. Using currentBackgroundColor for example, we could set the border color to be slightly darker than the background color (border-color: hsl(from currentBackgroundColor h s calc(l - 30))), or mix the background color with another color and then use that as the border color (border-color: color-mix(currentBackgroundColor, black, 30)).
But why stop there? Why not currentWidth, currentHeight, and so on?
The from-font CSS keyword
The from-font CSS keyword is exclusive to the text-decoration-thickness property, which can be used to set the thickness of underlines. If you’ve ever hated the fact that underlines are always 1px regardless of the font-size and font-weight, then text-decoration-thickness can fix that.
The from-font keyword doesn’t generate a value though — it’s optionally provided by the font maker and embedded into the font file, so you might not like the value that they provide, if they provide one at all. If they don’t, auto will be used as a fallback, which web browsers resolve to 1px. This is fine if you aren’t picky, but it’s nonetheless unreliable (and obviously quite niche).
We can, however, specify a percentage value instead, which will ensure that the thickness is relative to the font-size. So, if text-decoration-thickness: from-font just isn’t cutting it, then we have that as a backup (something between 8% and 12% should do it).
Don’t underestimate CSS units
You probably already know about vw and vh units (viewport width and viewport height units). These represent a percentage of the viewport’s width and height respectively, so 1vw for example would be 1% of the viewport’s width. These units can be useful by themselves or within a calc() function, and used within any property that accepts a <length> unit.
1lh: equal to the computed line-height (as long as you’re not trimming or adding to its content box, for example using text-box or padding, respectively, lh units could be used to determine the height of a box that has a fixed number of lines)
And again, you can use them, their logical variants (e.g., vi and vb), and their root variants (e.g., rex and rcap) within any property that accepts a <length> unit.
In addition, if you’re using container size queries, you’re also free to use the following container query units within the containment contexts:
1cqw: equal to 1% of the container’s computed width
1cqh: equal to 1% of the container’s computed height
1cqi: equal to 1% of the container’s computed inline size
1cqb: equal to 1% of the container’s computed block size
1cqmin: equal to 1cqi or 1cqb, whichever is smallest
1cqmax: equal to 1cqi or 1cqb, whichever is largest
That inherit() example from earlier, you know, the one that isn’t currently supported by any web browser? Here’s the same thing but with container size queries:
Or, since we’re talking about a container and its direct child, we can use the following shorter version that doesn’t create and query a named container (we don’t need to query the container anyway, since all we’re doing is stealing its units!):
However, keep in mind that inherit() would enable us to inherit anything, whereas container size queries only enable us to inherit sizes. Also, container size queries don’t work with inline containers (that’s why this version of the container is horizontally stretched), so they can’t solve every problem anyway.
In a nutshell
I’m just going to throw compute() out there again, because I think it’d be a really great way to get the values of other CSS properties:
button {
/* self could be the default */
border-radius: compute(height, self);
/* inherit could work like inherit() */
border-radius: compute(height, inherit);
/* Nice to have, but not as important */
border-radius: compute(height, #this);
}
But if it’s just not possible, I really like the idea of introducing more currentColor-like keywords. With the exception of keywords like from-font where the font maker provides the value (or not, sigh), keywords such as currentWidth and currentHeight would be incredibly useful. They’d make CSS easier to read, and we wouldn’t have to create as many custom properties.
In the meantime though, custom properties, aspect-ratio, and certain CSS units can help us in the right circumstances, not to mention that we’ll be getting inherit() in the future. These are heavily geared towards getting widths and heights, which is fine because that’s undoubtedly the biggest problem here, but hopefully there are more CSS features on the horizon that allow values to be used in more places.
Our latest update — Copenhagen — features a major redesign of Sketch’s UI. Redesigns like this don’t happen often. In fact, our last one was in 2020, when Apple launched macOS Big Sur.
Makes a lot of sense for an app that’s so tightly integrated to Mac to design around the macOS UI. Big Sur was a big update. Apple called it the biggest one since Mac OS X. So big, indeed, that they renamed Mac OS to macOS in the process. Now we have macOS Tahoe and while it isn’t billed the “biggest update since Big Sur” it does lean into an entirely new Liquid Glass aesthetic that many are calling the biggest design update to the Apple ecosystem since iOS 7.
Sketch probably didn’t “have” to redesign its UI to line up with macOS Tahoe, but a big part of its appeal is the fact that it feels like it totally belongs to the Mac. It’s the same for Panic apps.
The blog post I linked to sheds a good amount of light on the Sketch team’s approach to the updates. I came to the blog post to read about the attention they put into new features (individual page and frame link for the win!) and tightening up existing ones (that layer list looks nice), but what I really stayed for was their approach to Liquid Glass. Turns out they decided to respect it, but split lanes a bit:
Early on in the process, we prototyped various approaches to the sidebar and Inspector, including floating options (the new default in Tahoe) and glass materials. Ultimately, we went custom here, with fixed sidebars that felt less distracting in the context of a canvas-based design tool.
Spend a few seconds with an early prototype that leaned more heavily into Liquid Glass and it’s uber clear why a custom route was the best lane choice:
Still taken from one of the blog post’s embedded videos
Choosing a design editor can feel personal, can’t it? I know lots of folks are in the Figma Or Bust camp. Illustrator is still the favorite child for many, after all these… decades! There’s a lot of buzz around Affinity now that it’s totally free. I adopted Sketch a long time ago. How long? I dug up this dusty old blog post I wrote about Sketch 3 back in 2014, so at least 11 years.
But I’m more of a transient in the design editor space. Being a contractor and all, I have to be open to any app my clients might use internally, regardless of my personal preference. I’d brush up on Sketch’s UI updates even if it wasn’t my go-to.
For the past few months, I’ve been writing a lot of entries on pseudo-selectors in CSS, like ::picker() or ::checkmark. And, in the process, I noticed I tend to use the :open pseudo-selector a lot in my examples — and in my work in general.
Borrowing words from the fine author of the :open entry in the Almanac:
The CSS :open pseudo-selector targets elements that support open and closed states — such as the <details> and <select> elements — and selects them in their open state.
We expect that the <details> element gets a light blue background and dark red text when it is in an open state (everywhere but Safari at the time I’m writing this):
But what if we want to select the “closed” state instead? That’s what we have the:closed pseudo-class for, right? It’s supposed to match an element’s closed state. I say, supposed because it’s not specced yet.
But does it need to be specced at all? I only ask because we can still target an element’s closed state without it using :not():
/* When details is _not_ open, but closed */
details:not(:open) {
/* ... */
}
So, again: do we really need a :closed pseudo-class? The answer may surprise you! (Just kidding, this isn’t that sort of article…)
Some background
Talks surrounding :open started in May 2022 when Mason Freed raised the issue of adding :open (which was also considered being named :top-layer at the time) to target elements in the top layer (like popups):
Today, the OpenUI WC similarly resolved to add a :top-layer pseudo class that should apply to (at least) elements using the Popup API which are currently in the top layer. The intention for the naming and behavior, though, was that this pseudo class should also be general purpose. It should match any type of element in the top layer, including modal <dialog>, fullscreen elements, and ::backdrop pseudo elements.
This sparked discourse on whether the name of the pseudo-element targeting the top layer of any type of element (e.g., popups, pickers, etc.) should either be :open or :top-layer. I, for one, was thrilled when the CSSWG eventually decided on :open in August 2022. The name makes a lot more sense to me because “open” assumes something in the top layer.
To :close or :not(:open)?
Hold on, though! In September that same year, Mason asked whether or not we should have something like a :closed pseudo-class to accompany :open. That way, we can match elements in their “closed” states just as we can their “open” states. That makes a lot of sense, t least on the surface. Tab Atkins chimed in:
I love this definition, as I think it captures a concept of “openness” that lines up with what most developers think “open” means. I also think it makes it relatively straightforward for HTML to connect it to specific elements.
What do folks think?
Should we also talk about adding the corresponding :closed pseudo class? That would avoid the problem that :not(:open)can match anything, including things that don’t open or close.
And guess what? Everyone seemed to agree. Why? Because it made sense at the time. I mean, since we have a pseudo-class that targets elements in their :open state, surely it makes sense to have :closed to target elements in their closed states, right? Right??
No. There’s actually an issue with that line of reasoning. Joey Arhar made a comment about it in October that same year:
I opened a new issue about :closed because this doesn’t have consensus yet (#11039).
Wait, what happened to consensus? It’s the same question I raised at the top of this post. According to Luke Warlow:
Making :closed match things that can never be open feels odd. And would essentially make it :not(:open) in which case do we even need :closed? Like we don’t have a :popover-closed because it’s the inverse of :popover-open.
There is no :closed… for now
Fast forward one more month to November 2024. A consensus was made to start out with just :open and remove :closed for the time being.
Dang. Nevertheless, according to WHATWG and CSSWG, that decision could change in the future. In fact, Bramus dropped a useful note in there just a month before WHATWG made the decision:
Just dropping this as an FYI: :read-only is defined as :not(:read-write), and that shipped.
Which do you find easier to understand?
Personally, I’m okay with :closed — or even using :not(:open) — so far as it works. In fact, I went ahead swapped :closed for :not(:open) in my ::checkmark and ::picker() examples. That’s why they are they way they are today.
But! If you were to ask me which one comes easier to me on a typical day, I think I would say :closed. It’s easier for to think in literal terms than negated statements.
What do you think, though? Would you prefer having :closed or just leaving it as :not(:open)?
If you’re like me and you love following discussions like this, you can always head over to CSSWG drafts on GitHub to watch or participate in the fun.
This is a series! It all started a coupleof articles ago, when we found out that, according to the State of CSS 2025 survey, trigonometric functions were the “Most Hated” CSS feature.
I’ve been trying to change that perspective, so I showcased several uses for trigonometric functions in CSS: one for sin() and cos() and another on tan(). However, that’s only half of what trigonometric functions can do. So today, we’ll poke at the inverse world of trigonometric functions: asin(), acos(), atan() and atan2().
CSS Trigonometric Functions: The “Most Hated” CSS Feature
Recapping things a bit, given an angle, the sin(), cos() and tan() functions return a ratio presenting the sine, cosine, and tangent of that angle, respectively. And if you read the last two parts of the series, then you already know what each of those quantities represents.
What if we wanted to go the other way around? If we have a ratio that represents the sine, cosine or tangent of an angle, how can we get the original angle? This is where inverse trigonometric functions come in! Each inverse function asks what the necessary angle is to get a given value for a specific trigonometric function; in other words, it undoes the original trigonometric function. So…
acos() is the inverse of cos(),
asin() is the inverse of sin(), and
atan() and atan2() are the inverse of tan().
They are also called “arcus” functions and written as arcos(), arcsin() and arctan() in most places. This is because, in a circle, each angle corresponds to an arc in the circumference.
The length of this arc is the angle times the circle’s radius. Since trigonometric functions live in a unit circle, where the radius is equal to 1, the arc length is also the angle, expressed in radians.
Their mathy definitions are a little boring, to say the least, but they are straightforward:
y = acos(x) such that x = cos(y)
y = asin(x) such that x = sin(y)
y = atan(x) such that x = tan(y)
acos() and asin()
Using acos() and asin(), we can undo cos(θ) and sin(θ) to get the starting angle, θ. However, if we try to graph them, we’ll notice something odd:
The functions are only defined from -1 to 1!
Remember, cos() and sin() can take any angle, but they will always return a number between -1 and 1. For example, both cos(90°) and cos(270°) (not to mention others) return 0, so which value should acos(0) return? To answer this, both acos() and asin() have their domain (their input) and range (their output) restricted:
acos() can only take numbers between -1 and 1 and return angles between 0° and 180°.
asin() can only take numbers between -1 and 1 and return angles between -90° and 90°.
This limits a lot of the situations where we can use acos() and asin(), since something like asin(1.2) doesn’t work in CSS* — according to the spec, going outside acos() and asin() domain returns NaN — which leads us to our next inverse function…
atan() and atan2()
Similarly, using atan(), we can undo tan(θ) to get θ. But, unlike asin() and acos(), if we graph it, we’ll notice a big difference:
This time it is defined on the whole number line! This makes sense since tan() can return any number between -Infinity and Infinity, so atan() is defined in that domain.
atan() can take any number between -Infinity and Infinity and returns angles -90° and 90°.
This makes atan() incredibly useful to find angles in all kinds of situations, and a lot more versatile than acos() and asin(). That’s why we’ll be using it, along atan2(), going forward. Although don’t worry about atan2() for now, we’ll get to it later.
Finding the perfect angle
In the last article, we worked a lot with triangles. Specifically, we used the tan() function to find one of the sides of a right-angled triangle from the following relationships:
To make it work, we needed to know one of its sides and the angle, and by solving the equation, we would get the other side. However, in most cases, we do know the lengths of the triangle’s sides and what we are actually looking for is the angle. In that case, the last equation becomes:
Triangles and Conic Gradients
Finding the angle comes in handy in lots of cases, like in gradients, for instance. In a linear gradient, for example, if we want it to go from corner to corner, we’ll have to match the gradient’s angle depending on the element’s dimensions. Otherwise, with a fixed angle, the gradient won’t change if the element gets resized:
.gradient {
background: repeating-linear-gradient(ghostwhite 0px 25px, darkslategray 25px 50px);
}
This may be the desired look, but I think that most often than not, you want it to match the element’s dimensions.
Using linear-gradient(), we can easily solve this using to top right or to bottom left values for the angle, which automatically sets the angle so the gradient goes from corner to corner.
.gradient {
background: repeating-linear-gradient(to top right, ghostwhite 0px 25px, darkslategray 25px 50px);
}
However, we don’t have that type of syntax for other gradients, like a conic-gradient(). For example, the next conic gradient has a fixed angle and won’t change upon resizing the element.
Luckily, we can fix this using atan()! We can look at the gradient as a right-angled triangle, where the width is the adjacent side and the height the opposite side:
You may be wondering: can’t we do that with a linear gradient? And the answer is, yes! But this was just an example to showcase atan(). Let’s move on to more interesting stuff that’s unique to conic gradients.
Pretty cool, right?. Sadly, Ana’s post is from 2021, a time when trigonometric functions were specced out but not implemented. As she mentions in her article, it wasn’t possible to create these gradients using atan(). Luckily, we live in the future! Let’s see how simple they become with trigonometry and CSS.
We’ll use two conic gradients, each of them covering half of the card’s background.
To save time, I’ll gloss over exactly how to make the original gradient, so here is a quick little step-by-step guide on how to make one of those gradients in a square-shaped element:
Since we’re working with a perfect square, we can fix the --angle and --rotation to be 45deg, but for a general use case, each of the conic-gradients would look like this in CSS:
The last example was all abou atan(), but I told you we would also look at the atan2() function. With atan(), we get the angle when we divide the opposite side by the adjacent side and pass that value as the argument. On the flip side, atan2() takes them as separate arguments:
atan(opposite/adjacent)
atan2(opposite, adjacent)
What’s the difference? To explain, let’s backtrack a bit.
We used atan() in the context of triangles, meaning that the adjacent and opposite sides were always positive. This may seem like an obvious thing since lengths are always positive, but we won’t always work with lengths.
Imagine we are in a x-y plane and pick a random point on the graph. Just by looking at its position, we can know its x and y coordinates, which can have both negative and positive coordinates. What if we wanted its angle instead? Measuring it, of course, from the positive x-axis.
Well, remember from the last article in this series that we can also define tan() as the quotient between sin() and cos():
Also recall that when we measure the angle from the positive x-axis, then sin() returns the y-coordinate and cos() returns the x-coordinate. So, the last formula becomes:
And applying atan(), we can directly get the angle!
This formula has one problem, though. It should work for any point in the x-y plane, and since both x and y can be negative, we can confuse some points. Since we are dividing the y-coordinate by the x-coordinate, in the eyes of atan(), the negative y-coordinate looks the same as the negative x-coordinate. And if both coordinates are negative, it would look the same as if both were positive.
To compensate for this, we have atan2(), and since it takes the y-coordinate and x-coordinate as separate arguments, it’s smart enough to return the angle everywhere in the plane!
Let’s see how we can put it to practical use.
Following the mouse
Using atan2(), we can make elements react to the mouse’s position. Why would we want to do that? Meet my friend Helpy, Clippy’s uglier brother from Microsoft.
Helpy wants to always be looking at the user’s mouse, and luckily, we can help him using atan2(). I won’t go into too much detail about how Helpy is made, just know that his eyes are two pseudo-elements:
To help Helpy, we first need to let CSS know the mouse’s current x-y coordinates. And while I may not like using JavaScript here, it’s needed in order to pass the mouse coordinates to CSS as two custom properties that we’ll call --m-x and --m-y.
const body = document.querySelector("body");
// listen for the mouse pointer
body.addEventListener("pointermove", (event) => {
// set variables for the pointer's current coordinates
let x = event.clientX;
let y = event.clientY;
// assign those coordinates to CSS custom properties in pixel units
body.style.setProperty("--m-x", `${Math.round(x)}px`);
body.style.setProperty("--m-y", `${Math.round(y)}px`);
});
Helpy is currently looking away from the content, so we’ll first move his eyes so they align with the positive x-axis, i.e., to the right.
.helpy::before,
.helpy::after {
rotate: 135deg;
}
Once there, we can use atan2() to find the exact angle Helpy has to turn so he sees the user’s mouse. Since Helpy is positioned at the top-left corner of the page, and the x and y coordinates are measured from there, it’s time to plug those coordinates into our function: atan2(var(--m-y), var(--m-x)).
.helpy::before,
.helpy::after {
/* rotate the eyes by it's starting position, plus the atan2 of the coordinates */
rotate: calc(135deg + atan2(var(--m-y), var(--m-x)));
}
We can make one last improvement. You’ll notice that if the mouse goes on the little gap behind Helpy, he is unable to look at the pointer. This happens because we are measuring the coordinates exactly from the top-left corner, and Helpy is positioned a little bit away from that.
To fix this, we can translate the origin of the coordinate system directly on Helpy by subtracting the padding and half its size:
This is a somewhat minor improvement, but moving the coordinate origin will be vital if we want to place Helpy in any other place on the screen.
Extra: Getting the viewport (and anything) in numbers
I can’t finish this series without mentioning a trick to typecast different units into simple numbers using atan2() and tan(). It isn’t directly related to trigonometry but it’s still super useful. It was first described amazingly by Jane Ori in 2023, and goes as follows.
If we want to get the viewport as an integer, then we can…
And now: the --int-width variable holds the viewport width as an integer. This looks like magic, so I really recommend reading Jane Ori’s post to understand it. I also have an article using it to create animations as the viewport is resized!
What about reciprocals?
I noticed that we are still lacking the reciprocals for each trigonometric function. The reciprocals are merely 1 divided by the function, so there’s a total of three of them:
The secant, or sec(x), is the reciprocal of cos(x), so it’s 1 / cos(x).
The cosecant, or csc(x), is the reciprocal of sin(x), so it’s 1 / sin(x).
The cotangent, or cot(x) is the reciprocal of tan(x), so it’s 1 / tan(x).
The beauty of sin(), cos() and tan() and their reciprocals is that they all live in the unit circle we’ve looked at in other articles in this series. I decided to put everything together in the following demo that shows all of the trigonometric functions covered on the same unit circle:
That’s it!
Welp, that’s it! I hope you learned and had fun with this series just as much as I enjoyed writing it. And thanks so much for those of you who have shared your own demos. I’ll be rounding them up in my Bluesky page.
CSS Trigonometric Functions: The “Most Hated” CSS Feature
A few weeks ago, Quiet UI made the rounds when it was released as an open source user interface library, built with JavaScript web components. I had the opportunity to check out the documentation and it seemed like a solid library. I’m always super excited to see more options for web components out in the wild.
Unfortunately, before we even had a chance to cover it here at CSS-Tricks, Quiet UI has disappeared. When visiting the Quiet UI website, there is a simple statement:
Unavailable
Quiet UI is no longer available to the general public. I will continue to maintain it as my personal creative outlet, but I am unable to release it to the world at this time.
Thanks for understanding. I’m really sorry for the inconvenience.
The repository for Quiet UI is no longer available on Quiet UI’s GitHub, and its social accounts seem to have been removed as well.
The creator, Cory LaViska, is a veteran of UI libraries and most known for work on Shoelace. Shoelace joined Font Awesome in 2022 and was rebranded as Web Awesome. The latest version of Web Awesome was released around the same time Quiet UI was originally announced.
According to the Quiet UI site, Cory will be continuing to work on it as a personal creative outlet, but hopefully we’ll be able to see what he’s cooking up again, someday. In the meantime, you can get a really good taste of what the project is/was all about in Dave Rupert’s fantastic write-up.
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.
/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:
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:
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: