Open kizu opened 1 day ago
Would infinities be automatically included?
round(down, 0, 2 3 5 7 11); /* -infinity or nan? */
Would the list need to be in non-decreasing order, or leave it up to the browser to sort it?
Would the list accept duplicates? I guess it would be a way to have a finite list with one item...
But IMO it can be confusing that providing 2 values results in a 2-values finite list, while providing 1 value results in an infinite list.
It may be better to introduce a new syntax. It could also be useful to round to intervals, e.g.
round(var(--num), [0, 2 .. 3, 4, 5 .. 6])
All good questions!
Would infinities be automatically included?
round(down, 0, 2 3 5 7 11); /* -infinity or nan? */
Hmm, in this case I'd expect 2
— the closest available value even if it is higher than the target. The goal is usually to have a set of “design tokens” and choose only from them, taking the “closest”, so in the regular cases the up
/down
are for choosing one from two closest items. In this case, the 2
is the only available item, and is the closest.
Would the list need to be in non-decreasing order, or leave it up to the browser to sort it? Would the list accept duplicates? I guess it would be a way to have a finite list with one item...
Hmm, an author in me wants the browser to do the work for me, and do both: remove the duplicates, and then sort the array. Is this a potential performance issue? I imagine 99% of real-world cases won't have too many items there, and if we really don't want people to sort thousands of items, we could have a certain limit in-place.
I remember the toggle()
specified in a way where it should not contain duplicates, but it was never implemented by anyone, and I don't know of any other precedents that could help with this.
But the author experience would be for sure much better if the duplicates and non-strict order is allowed.
But IMO it can be confusing that providing 2 values results in a 2-values finite list, while providing 1 value results in an infinite list.
I can see how this could be confusing, yes, but I wonder if this edge case is too unrealistic to try, and solve it. I'd go with it producing a finite list of 1 element, which is, uh, just that value, but better than falling back to infinite list, as it won't ever be something an author wants.
It may be better to introduce a new syntax. It could also be useful to round to intervals, e.g.
round(var(--num), [0, 2 .. 3, 4, 5 .. 6])
Oh, so 2 .. 3
in this case will be something like “if a value falls between these, keep the value intact”, like a mini-clamp()
inside this list?
This is an interesting idea, and I can see how introducing a new syntax can help to add things like this one. Although, I am not sure if I remember use cases for this specific case, but if there are some — we can consider it. But maybe this is something that could be potentially solved better with native functions and conditionals?
Many tools could be used for the same job, and I wonder if real-life cases that will need something like that will always want something even more complicated. Like, if the value is higher than 6, then do round(var(--num), 2)
— this is easy to do with if()
if we'd have number comparisons in it, and could be more expressive than introducing a new syntax to round()
.
Would infinities be automatically included?
round(down, 0, 2 3 5 7 11); /* -infinity or nan? */
I'd say NaN. Infinitely small or large numbers are unexpected in the given use cases.
Would the list need to be in non-decreasing order, or leave it up to the browser to sort it?
For author's comfort, the UA should do the sorting. Though authors should still be encouraged to sort the values in increasing order.
Would the list accept duplicates? I guess it would be a way to have a finite list with one item...
It should accept duplicates, but rather to be more resilient against mistakes than for providing a way for a single-item list. There's presumably no use case for a list with a single item. And differenciating finite and infinite lists by syntax is less error-prone.
But IMO it can be confusing that providing 2 values results in a 2-values finite list, while providing 1 value results in an infinite list.
It may be better to introduce a new syntax.
As noted above, I agree on that.
Sebastian
the closest available value even if it is higher than the target
I disagree, disobeying the rounding strategy seems very unexpected.
I'd say NaN. Infinitely small or large numbers are unexpected in the given use cases.
The advantage of infinities is that then you can use a min/max/clamp wrapper to set the right limits. Though we could require the author to explicitly add infinities into the list in order to avoid NaN.
I’m not at all against solving this use case. Maybe round()
is indeed the best place to do so.
In #2826 re random()
, I suggested a function called choose()
or select()
to get a single item from a list, but this focused on choosing by index and did not consider choosing by value. However, I think that feature could be added therein just as well.
PS: Contrarily, med()
and mode()
in #905 and mid()
etc. in #4700 naturally considered only the list values, i.e. they supported no needle to compare with.
This use-case sounds potentially useful. I'd be willing to spec this if the WG thinks it's worthwhile.
Sorting or not is actually interesting. If we sort, then two values that are expressed in different units might exchange position, if the two units don't scale equally. That may or may not be what the author intended. If we don't sort, we presumably handle misordering the same way that gradients do, by bumping the later one in the list up to the position of the earlier one.
Whether we sort or not, tho, simple cases can be swapped to the other semantic with strategic use of min()/max(). Tho I suppose defaulting to sorting is easier to handle slightly more complex cases; you can regain the gradient behavior by changing, say, A B C
to A max(A, B) max(A, B, C)
; repetitive but doable. If we default to gradient behavior, tho, regaining the sorting behavior is a lot more complex; I think the simplest way to sort A B C
is min(A, B, C) calc(A + B + C - min(A, B, C) - max(A, B, C)) max(A, B, C))
, ugh, and it gets worse with 4 values.
I do think it's probably worth putting this under a new function name, rather than reusing round(); this is kinda rounding but not quite, especially if we allow ranges where the value is unchanged. This also avoids us having to be consistent with round() for the round(down, 0, 2 3 5)
case - we can just say that this resolves to 2, and you can manually use -infinity
if you want.
A separate function could work as well, yes.
Another thing that I forgot to mention, but about which I was thinking when initially coming up with this proposal: two other proposals by @LeaVerou :
https://github.com/w3c/csswg-drafts/issues/10034 — also talking about a scale, but with differences (it is continuous, from what I understood, and for colors the order for sure matters, as we can't really sort them).
https://github.com/w3c/csswg-drafts/issues/9992 — I can imagine these work ~similar to what I propose (when the default
is not specified, and we try to access by unknown key, choose the closest). The main difference is that in 9992 there are keys with their associated values, where we match by key and then take a value; I guess the scale I'm talking here about could be expressed as { 1: 1; 2: 2; 3: 3; 5: 5; 7: 7; 11: 11; }
and so on, but that's rather cumbersome.
And another thought that I had: I can see similarities with type grinding, where a registered custom property for an <integer>
, with a <number>
assigned, converts it into an acceptable value. The current @property
does not allow defining the rounding strategy in this case, but I wonder if what I want could've been expressed as a custom type: define a subtype of a <number>
or <dimension>
, but with a limited possible values, so when assigning to it, it would “choose” the closest. But, I guess, working with a type like that would be rather cumbersome as well: need to register another property, and use as an intermediate step between the value you want to round and where you want to use it.
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()
isround(<rounding-strategy>?, A, B?)
whereA
andB
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 theA
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 infinite1 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 doThis way, if we'd provide
--foo: 10
, it will round to the closest prime, resulting in11
. 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:
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 aboutround()
as a possible alternative, as I was stuck with it rounding to an infinite scale. Butclamp()
works very differently fromround()
, 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 augmentinground()
.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.