-
Notifications
You must be signed in to change notification settings - Fork 757
Description
Background
This is a proposal that I briefly discussed with @fantasai at CSWG F2F in A Coruña this year. The problem it tries to solve came up when I was writing my latest article, so I finally found time to write it.
One of the motivations for this proposal is a problem that was, for example, mentioned by @matthiasott in his talk at CSS Day 2024 (a bit after 39:31), where he argues that using container query units for fluid type, while possible, leads to too many font-sizes on the page, making it not possible to create a harmonic type scale.
But what if we could augment the existing round() to help with this and other similar cases?
Proposal
The current syntax of round() is round(<rounding-strategy>?, A, B?) where A and B are calculations that must share a type and resolve to any can resolve to any <number>, <dimension>, or <percentage>.
My proposal is to change it to round(<rounding-strategy>?, A, B*) — use * instead of ? for the last argument.
When only one value is provided as B, round() will work the same as now.
When multiple values are provided to B, the values would be treated as a scale of the only values that the A should be rounded to.
These values must be the only possible outcomes of the round() function: this is not rounding the value to either of the values in the regular sense, but using the provided values as a finite scale.
If we were to think of a single B, it would represent an infinite linear scale, for example the default 1 argument there represents an infinite 1 2 3 4 N scale, but if we'd want to round to a finite scale, for example, to find the closest prime number to a certain list of them, we could do
round(var(--foo), 1 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101);This way, if we'd provide --foo: 10, it will round to the closest prime, resulting in 11. And if we will provide something that is equally close to two values, like --foo: 6, then the <rounding-strategy>? will come into play: we could choose how exactly we'd want to handle cases like it — round things to the closest, or ceil/floor it.
Implementation-wise, this should not be too complex: something like a binary search of the first argument with the provided list could be good enough.
Other use-cases
Aside from the typographic scale and choosing the closest prime number, I remember stumbling upon many different use cases.
One prominent one: regular spacing scale. Various design systems can have a scale like 2px 4px 8px 12px 16px 20px 24px 32px 48px for spacing, and similar to the Matthias's case with fluid typography, it would be great to evaluate some container query length units to the closest value from such a scale.
There were other cases I encountered, but at the moment of writing this proposal I don't remember them: will update if I'll stumble upon them later, and if anyone had them as well — drop them in the comments, I'll update the proposal with them as well.
Current Workaround
Today, the simplest way we can attempt to approach this with the current CSS is by using a rather complicated complex conditional calculation which I described in my article. For the above list of primes (with some lower ones removed), it looks like this:
--limit: 102;
--closest-prime: calc(
var(--limit)
-
max(
min(1, 11 - var(--x)) * (var(--limit) - 11),
min(1, 13 - var(--x)) * (var(--limit) - 13),
min(1, 17 - var(--x)) * (var(--limit) - 17),
min(1, 19 - var(--x)) * (var(--limit) - 19),
min(1, 23 - var(--x)) * (var(--limit) - 23),
min(1, 29 - var(--x)) * (var(--limit) - 29),
min(1, 31 - var(--x)) * (var(--limit) - 31),
min(1, 37 - var(--x)) * (var(--limit) - 37),
min(1, 41 - var(--x)) * (var(--limit) - 41),
min(1, 43 - var(--x)) * (var(--limit) - 43),
min(1, 47 - var(--x)) * (var(--limit) - 47),
min(1, 53 - var(--x)) * (var(--limit) - 53),
min(1, 59 - var(--x)) * (var(--limit) - 59),
min(1, 61 - var(--x)) * (var(--limit) - 61),
min(1, 67 - var(--x)) * (var(--limit) - 67),
min(1, 71 - var(--x)) * (var(--limit) - 71),
min(1, 73 - var(--x)) * (var(--limit) - 73),
min(1, 79 - var(--x)) * (var(--limit) - 79),
min(1, 83 - var(--x)) * (var(--limit) - 83),
min(1, 89 - var(--x)) * (var(--limit) - 89),
min(1, 97 - var(--x)) * (var(--limit) - 97),
min(1, 101 - var(--x)) * (var(--limit) - 101)
)
);This works! But this is not something that is easy to write or maintain by hand.
Alternatives Considered
Initially, I was thinking about either introducing a new function, or looking if we could somehow do this with clamp(). For some reason, I was not thinking about round() as a possible alternative, as I was stuck with it rounding to an infinite scale. But clamp() works very differently from round(), where it keeps the original value if it fits into the provided range. But we're really rounding it to a scale, and it was @fantasai that proposed to think about just augmenting round().
Out of Scope
I think there is something about being able to specify an alternative, non-linear infinite list, maybe in a form of an equation similar to the one in the nth-child, but maybe a bit more complex.
It would be great to be able to round something to a scale like 2 4 8 16 32 64 etc, or be able to specify a fallback if the value falls outside of the chosen finite scale.
In the future, I think these would be nice to have, but I'd propose to have separate issues discussing them and bikeshedding their syntax. A finite scale is a common enough case, and seems to be simple enough to implement, that I'd want us to first focus on it.