Hi,
I see many authors (myself included) rely on JavaScript's Math.random()
to generate a number they can then use to randomize colors, positions, animation delays etc. While it's fairly straightforward to do so (notably with custom properties), a potential random
function (or keyword) in CSS would make a lot of sense, especially in conjunction with calc()
and friends.
AFAICT @tabatkins proposed something similar quite a few years ago, but I don't think we've discussed it recently, and I think we should reevaluate this option now that we have more context and background on how custom properties and mathematical expressions are being used in CSS.
I don't think random() would make sense. When should it be evaluated? Whenever the style of an element is resolved?
That's right now implementation dependent, there are engines that optimize style changes better than others.
In my previous musings on the topic, I stated that there are two ways we can evaluate random(), both of which are useful:
The first means that .foo:hover { color: rgb(random(0, 255), 0, 0); }
evaluates to the same color for every .foo element, and doesn't change if you repeatedly hover/unhover. The second means it'll be different on each .foo element, but will still be consistent if you repeatedly hover/unhover a given element.
There's possibly a third, even less stable mode: evaluated freshly at "application" time, whenever the rule would trigger a transition due to it winning the cascade over a different specified value. This is different per element, and is different each time you hover the element.
We'd also want a few different types of randomness; numeric ranges are okay, but getting a random value from a list is very useful and handles colors well, too.
@tabatkins I'm definitely in favor of the third option. Would you mind clarifying what you mean by "getting a random value from a list" though?
@bendc Probably like random([1, 2, 4])
when each gets 1/3 probability.
I鈥檇 say also provide an option to distinguish between int / float random.
There's possibly a third, even less stable mode: evaluated freshly at "application" time, whenever the rule would trigger a transition due to it winning the cascade over a different specified value. This is different per element, and is different each time you hover the element.
This is not deterministic, right?
What do you mean by that?
That the time you apply properties as a result of a style change is not defined, in the sense that engines can do wasted work for that in different ways.
For example, if you have .foo div
and you have <div><span></span></div>
, adding the class foo
to the <div>
will cause some engines to re-resolve the style of the <span>
and not in others, even though the <span>
s style didn't change.
That's why I didn't base it on style application, which is indeed undefined, but based it on transition triggering, which is. (The timing is still a little undefined, but assuming you can approach steady state, whether or not a transition occurs is well-defined.)
What do you mean with 'transition triggering'? You mean only resolve random()
iff any other style actually changed? You need to resolve the style to know that, so that'd be slow.
If a random()-containing declaration would newly apply (such that a transition on the property would trigger, if you treated the random()-containing declaration as being a different computed value from anything but itself), then at that point you resolve the random() to a fresh random value and apply it.
What happens if the OM touches that declaration? Should we re-compute random() to elements that already match it? Such a model sounds complicated to me.
I think the most logical option from a usage standpoint would be to calculate the random number when a rule is applied to an element. It should only be recalculated if that rule has been removed and then added again (class addition/removal, hover triggered etc).
That would allow for the random value to persist and update controllably rather than in some undefined fashion whenever the browser feels like it and would mean multiple elements targeted by the same selector would receive a different random value. I'm not sure how easy it would be for a browser to implement though.
It seems like authors might want different things:
Rather than invent different random functions, I think this functionality is better served with CSS variables, and the ability to set those from script.
The issue with that approach is having to generate a custom property for every element you are targeting. At that point you may as well just style the elements directly with JS.
A CSS solution is much more convenient. I believe it would be worth developing some use cases to determine the requirements.
(Note: significant reasoning and discussion here; simple proposal sketch in the next post if you want to just skip to that.)
Yeah, just using script-set variables only solves the "randomly determined at parse time" case. It doesn't help if you want to get a different random value per element, unless you go significantly greater lengths.
Dealing with random value "persistence" is the hard problem to solve here. We need to define precisely what data gets fed into the random generator; or more simply, what data we use to cache the random values, so we can retrieve the same value when we reevaluate style.
That gets us to the rub; how are you caching the values when you have a rule like
.foo {
color: rgb(rand-int(0, 255) rand-int(0, 255) rand-int(0, 255));
}
The three rand-int calls all need to give distinct values, but need to be persistent across style recalcs. Let's also assume these are meant to be different values on each element, so you can't just do parse-time resolution and call it a day; the functions have to persist until probably computed-value time, at which point they turn into an integer.
Obviously, the element itself is part of the cache key.
What else? Can't just use property; there are three instances and they need to all be distinct. Maybe property + occurrence count (first, second, third)? That still means that different rules would reuse cached values, which would probably be unexpected: a hover rule setting it to rgb(127 rand-int(0, 255) rand-int(0, 255))
would use the same value for Green as the other style did for Red, and the same for Blue that the other did for Green.
Maybe key it off of rule as well, but we don't have a good, stable notion of rule identity. Changes to a stylesheet can cause a reparse and create totally new objects in the CSSOM, so using OM object-identity is probably bad. Dont' want to, like, hash the rule contents either, as it would mean that changing other properties would alter the random value.
Ultimately, I think the only reasonable cache key, that works reliably and with a minimum of surprise, is an author-provided custom identifier. As long as you provide the same identifier, the random() function should resolve to the same value.
One more wrinkle, then: we don't want to expose internal details of the random data, to allow implementations flexibility to change and be different from each other. Thus, we don't want to reuse the same random number for different random functions; the obvious trivial implementation of rand-int() would just modulo a random 32-bit int or something, and using the same value for 1-3 as 1-5 will expose information about the number being used (its value modulo 3 and modulo 5). So we also need to take the range start and end as cache keys.
So: the random values are generated on demand, and cached against:
This, in addition to being consistent and understandable, has the additional benefit that you can jam a random() into a custom property, and it'll Just Work漏 without you having to think about it; each use of the var() will get the same value. None of the other possible solutions do this, afaict.
Now, output spaces. I think it's valuable to have both random integers and random numerics (reals, lengths, angles, etc). Just using a random real in a place that expects integers will not work as expected: rand-real(1, 3) will generate a number that rounds to 1 25% of the time, that rounds to 2 50% of the time, and that rounds to 3 25% of the time, a far cry from the 33% you'd like each integer to be generated. You need a floor() function to write a rand-int() out of rand-real(), and it's non obvious how to do so; I write Math.randInt()
in JS all the time, and never trust my impl until I run a statistical test on it.
I don't think we need integer-valued dimensions; that's rarely, if ever, actually useful (as opposed to rounding to larger integers, like a random multiple of 100px), and you can just use calc(1px * rand-int(1,10))
to get it if you want. (Or calc(100px * rand-int(3, 5))
for a more realistic case - 300px, 400px, or 500px.)
The real-valued function should accept any numeric value as its start/end, and just require that their types match. (Specifically, that adding the two types is successful, with a null percent hint.)
Lists of values (like, grab a random color from these possibilities) is a valid use-case, but I don't want to solve it directly via a different random function. I think a reasonable use-case is to have sets of values that are valid to use together, but that you want a random choice of. As such, I think we should add an nth(n, val1, val2, ...)
function, and then we can just use rand-int() on it if we want.
So, proposal:
rand-int(<custom-ident> per-element?, <integer>, <integer>)
rand-val(<custom-ident> per-element?, <numeric>, <numeric>)
nth(<integer>, <value>, <value>, <value>, ...)
Like calc(), the rand-* functions evaluates at computed-value time if possible (if their arguments can be resolved at computed-value time), and at used-value time otherwise. They resolve to either a random integer between the first and second value (inclusive at both ends, so rand-int(foo, 1, 3)
will resolve to 1, 2, or 3), or a random real-valued value between the first and second value (inclusive of start, exclusive of end, so rand-val(foo, 100px, 200px)
will give a random length >= 100px and < 200px).
For either function, you must supply a <custom-ident>
as the first argument. The ident is meaningless, but it ensures that you'll get the same random value from a given function regardless of when it's evaluated - every identical rand-*() invocation, with the same custom-ident, start, and end, will resolve to the same value.
If you additionally pass the per-element
keyword, then the random value will be different on every element the property is applied to. (But, again, is guaranteed to be the same on every invocation of an identical rand-*() function on that element.)
rand-int()
is an <integer>
. rand-val()
is the addition of its arguments' types.
The UA maintains a cache of random values, keyed by a tuple of:
per-element
is specified; null
otherwiseWhenever the UA wants to resolve a rand-* function, it grabs a value from the cache if it exists; otherwise generate a fresh random value and put it in the cache. Then it turns that random value into a value in the appropriate range, via whatever means you want. (More than likely, a simple modulo and addition for integers, and a rescale and addition for reals.)
The nth()
function takes an integer, and a succession of comma-separated values. It returns the nth value (1-indexed, as usual for CSS). You can use this with rand-int()
to get a random value from a list of pre-supplied ones, such as a set of chosen colors.
What would happen in case of unclear integers, e. g. rand-int(foo, 10pt, 5mm)
?
Could this be solved with counters instead, which are always integers? counter-shuffle: foo, bar 5, baz 1 5;
would set _foo_ to a random value between (initial) 0 and its current value, _bar_ to a random value between 0 (or its current value?) and 5, and _baz_ to a random value between 1 and 5. For this to work, of course, counters must be legal inside calc()
.
random(a, b, id)
would require _a_ and _b_ to be the same dimension and yield a random value between them. Rounding functions have already been proposed to deal with finer nuances, e. g. round(random(10pt, 5mm, foo), 1px)
.
Thanks a lot for following up on this thread, super excited to see this!
Before I comment on the proposals above, I just wanted to make sure we're philosophically on board with the idea of extending the capabilities of CSS as such. I guess it's hard to find an argument _against_ it since we already have calc()
but, adding something like rand()
presumably means adding many other things as well (rounding, lists, etc.) and it's undeniable that it's a pretty significant departure from today's CSS. I'm pretty sure, for example, that implementing these functions will result in many requests for a console.log
-like feature in CSS.
Is that a problem per se? I'm not sure, but I'd rather make sure before we jump on the implementation details :)
What would happen in case of unclear integers, e. g. rand-int(foo, 10pt, 5mm)?
rand-int() is integers only, not dimensions. But for rand-val(), it's not resolved until computed or used value time, so the start and end points are already fully absolutized, and thus totally clear.
Could this be solved with counters instead, which are always integers?
Counters are... not something we want to build other features on top of. They have a lot of strange quirks.
Before I comment on the proposals above, I just wanted to make sure we're philosophically on board with the idea of extending the capabilities of CSS as such.
I am, but I'm utopian about these things. ^_^ I've been thinking about randomness in CSS for many years. At bare minimum, exploring this space will serve as a great case-study for what we need to be sure that we expose for Houdini Custom Functions; I want to ensure that authors could create a --rand-int()
function with this same functionality. So even if we never get this natively implemented, it'll still be useful.
@emilio was concerned about the design implying a global hash of ident=>random state, as it would imply trouble with parallelizing.
This shouldn't be the case; the "random()" function doesn't actually need to invoke a random generator (and, as far as I can tell, can't do so in any reasonable capacity). Instead, it's a hash function + mapping the result into the specified numeric range, relying on the following information only, all of which should be parallelism-friendly afaict:
per-element
is specified, some unique identifier from the element in questionConcerns, @emilio ?
Not particularly, I guess.
@bendc Yes and you should keep using Math.random() for this. It is not CSS's role to do compute. Please don't mix things up.
HTML is for markup.
CSS is for markup styling.
JavaScript is for compute.
Each have their reasons to be, if you need compute, use JavaScript.
I'd appreciate if you did not add more bloat to the already very bloated web.
I've been looking for something like Math.random()
in CSS for a few years, and experimenting with different use cases and ways they can be achieved. If I were to set this up today I'd use CSS variables as the handoff between JavaScript and CSS, which also gives you full control over how and when these values are calculated. Here's a demo:
<div>I use random from CSS</div>
<div style="--random-bg: 360;">I use random from DOM</div>
<div class=load>I am randomized once per load event</div>
<div class=click>I am randomized once per click event</div>
<style>
div {
--random-bg: 360;
background-color: hsl(calc(var(--random-bg) * 1deg), 75%, 50%);
}
.load {
--randomized-on-load: 360;
background-color: hsl(calc(var(--randomized-on-load) * 1deg), 75%, 50%);
}
.click {
--randomized-on-click: 360;
background-color: hsl(calc(var(--randomized-on-click) * 1deg), 75%, 50%);
}
</style>
<script type=module>
import computedVariables from 'https://unpkg.com/computed-variables/index.es.js'
computedVariables(
'--random-',
value => Math.random() * value,
window,
['load']
)
computedVariables(
'--randomized-on-load',
value => Math.random() * value,
window,
['load']
)
computedVariables(
'--randomized-on-click',
value => Math.random() * value,
window,
['click']
)
</script>
You can create a CSS variable like --example: 10;
and use the Javascript function value => Math.random() * value
to generate a random number between 0
and the supplied value. In this demo I've created a few different background colors and picked random numbers between 0 and 360. The next part is where the control comes in:
It seems like everything people want is possible already! In the past I'd have said that this was a much-needed feature that should be added to CSS for animation and other things, but considering how flexible and powerful it is to use JavaScript's own Math.random()
with styles by way of CSS variables I think a lot of that pressure is removed.
It's also easy to create random choice function in JavaScript that can pick randomly from a list of values given in CSS too, like --choose: ['lime', 'hotpink']
and assign the chosen value to the CSS variable for CSS to make use of in styles.
@tomhodgins Thank you for your comment, I'm not necessarily an experienced web developer and was pointing this out solely from a software design perspective. I am glad it makes sense to web developers like you too.
Being able to have control over the randomization is important. @tabatkins raises all the scenarios, and the discussion of using variables from @smfr and @tomhodgins is important, but i feel being independent from scripting is necessary.
Anyways, i did some work on those ideas with AliceJS a while back: http://blackberry.github.io/Alice. Also a sample demo at http://blackberry.github.io/Alice/demos/fx/bounce.html.
@ldhasson Problem is, if you start with this, you're going to implement another JavaScript inside CSS.
If having separate things is a problem, then, HTML, CSS and JavaScript should not have been separated from the start. It does not make sense to re-invent HTML, CSS and JavaScript in each of them.
I am not sure i understand this argument as many features in CSS can be done in JavaScript. The separation shouldn't be about language syntax but about purpose, i.e., programmatic logic vs presentation attributes. jQuery did fading for years until baking it into CSS as animation did it better.
So a similar but different way of representing the different technologies and their purpose is that HTML is for structure and content, CSS is for presentation, and JS is for logic. I believe that from this view, randomness is a valid way to describe presentation.
To me, there are two random function types - Range, and Set. Given the nature of randomness and precision, it would also be natural to have rounding and min/max functions, but not necessary.
I provide two values and some point in between the two is chosen at random. This would require two values: <Number1>, <Number2>
rand(1, 10);
0.1443...
(1 + ((10-1) * 0.1443))
2.2987...
0.7321...
(5mm + ((-7.5vw - 5mm) * 0.7321))
calc()
I provide a series of values and one of the values is chosen at random. This may have any number of values passed as long as one is passed: <Number1>, [<Number2>, <Number3>...]
rand-set(1, 2, 4, 8);
0.788...
set[floor(0.788 * (set.length + 1))]
set[2]
4
rand-set(-2px, 4, 120vh, #ffa, translateX(20px))
0.929...
set[floor(0.929 * (set.length + 1))]
set[4]
translate(20px)
Notes: I would recommend that if the above syntax is considered, allowing for the final parameter to be a rounding function
I would also note that I don't believe there is a need however to distinguish between integers and non-integers. Decimals, for example, are allowed in font sizes, color values, and otherwise. Needing further precision would be a rounding function, not a randomness function.
Additionally, I would suggest that the random value is calculated on a per-element basis at the moment the style is applied. This allows for more concise and powerful syntax (ex: span {left: rand(100px, 1000px)}
would be calculated separately for each span element). If the user wishes to share a single rand number across multiple definitions, that would be done with CSS variables. Another side-effect would be the re-calculating of random elements when a class is added, another item which would be a desirable default. Again CSS variables would allow the user around this calculation.
As to the general appropriateness -
To me, if I am describing a document's color and the website's design requires a random color paired with a calculated accent color, it would be improper of me to use JavaScript as the design is not part of the application logic. If I want to generate a randomized starfield where the content is located in stars across the page, that is style, not application logic.
The purpose of the specification is to allow a better description of the presentation semantics that were not addressed in CSS1. Using JavaScript to describe the presentation of a site is, in my opinion, the least desirable option.
Edit: Updated random functions
While the CSSWG just decided #4688 that a direct equivalent of ECMA-262始s Math.random()
function, which works on a range with static minimum 0.0 and maximum 1.0, was not appropriate for CSS, I think the more explicit variants just described by @DavidBradbury could be acceptable and useful in CSS.
The range-based variant works similar to means, of which we only have the _quadratic mean_ yet, called hypot()
. We probably should add it together with function(s) for _arithmetic mean_ and _geometric mean_ at least, or not at all. #4700
The set-based variant works like existing min()
and max()
. #544 Similar functions would be _mode_ (for the most frequent value), _median_ (for the central value) and _index_ or _choose_ (for a specific value). #905
@DavidBradbury Your set distributions are not uniform. The [0,1) random number should be multiplied by the length of the list (not minus 1), then floor (not round).
Also, I don't think "allowing for the final parameter to be a rounding function" is useful. What could be useful would be allowing an easing function to define the cumulative distribution function, i.e. linear
would be a uniform distribution. But some cubic beziers aren't non-decreasing, these would be invalid.
"the random value is calculated on a per-element basis at the moment the style is applied". Not sure this is well defined.
@Crissov Min, max and mean are deterministic statistics of a sample, this issue is about randomness, I don't see the relation. Anyway, you can calculate your custom mean with math functions.
Minimum and maximum select a single value out of a set. They are deterministic, because values can be compared pair wise to order them, but otherwise this is no different from selecting a random value out of a set. That means, random(red, green, blue, yellow)
would probably need to work as well.
@Loirooriol - Good points, updated the maths.
@Crissov - I think my only concern with only implementing random sets by itself is that invariably someone will end up needing to list something out like random(0,1, 2, 3, 4, 5... 358, 359)
or similar. That said, it's certainly better than no implementation (And the more likely hack that's less verbose is to random() a sufficient list of decimals and multiply that in a calc() function). It's mentioned in #4688 that
random() can't be done; it's a totally different topic.
Is there anywhere that expands on that?
While the CSSWG just decided #4688 that a direct equivalent of ECMA-262始s Math.random() function, which works on a range with static minimum 0.0 and maximum 1.0, was not appropriate for CSS,
That's not what we decided. I purposely omitted random()
from the discussion about other math functions because it's not a math function - it's not a pure function of its arguments, but rather a stateful function that you need to carefully control the invocation of; that's trivial in a traditional eager language like JavaScript, somewhat harder in a lazy language like Haskell, and very complex in a declarative language like CSS.
I've already discussed earlier in the thread what my plans are for random(). At this point I'm inclined instead to first make sure that Houdini Custom Functions can control their evaluation carefully enough that they can create a full-powered random function themselves. Assuming that works out, I can revisit making a standardized version.
At this point I'm inclined instead to first make sure that Houdini Custom Functions can control their evaluation carefully enough that they can create a full-powered random function themselves. Assuming that works out, I can revisit making a standardized version.
Does this mean providing a mechanism in browsers so CSS authors can experiment with supporting their own --random-range()
and --random-set()
and --own-custom-random()
functions in their stylesheets to discover which ideas might be beneficial to lock into the platform later? Who knows, maybe the way people use --random-range()
ends up exclusively for generating colors where a --random-color()
function or some other abstraction ends up fitting what you're actually using the randomness for even better?
Precisely, yes. My exploration of randomness in CSS indicates that there's a lot of potential directions to take the functions in, and I'm not sure which ones are most useful to standardize. Better to let the community tell me what's best, and still let them take care of their own niche cases as well. ^_^
Can someone give some good examples of where they might use this that isn't generating random colours?
@grorg Maybe random positions. Like placing decorative dots in different places each time you load the page.
@grorg Randomly assign corner-radius values to a container 鈥斅爏omething I am trying to design with at the moment...
I am trying to make a stack of cards look like they were placed on the page, so trying to add random rotations to each card. It would be ideal to be able to do this in CSS only. (think like a memory card game)
Hey, reminds me of AliceJS :) https://blackberry.github.io/Alice/demos/index.html#moredemos
Most helpful comment
In my previous musings on the topic, I stated that there are two ways we can evaluate random(), both of which are useful:
The first means that
.foo:hover { color: rgb(random(0, 255), 0, 0); }
evaluates to the same color for every .foo element, and doesn't change if you repeatedly hover/unhover. The second means it'll be different on each .foo element, but will still be consistent if you repeatedly hover/unhover a given element.There's possibly a third, even less stable mode: evaluated freshly at "application" time, whenever the rule would trigger a transition due to it winning the cascade over a different specified value. This is different per element, and is different each time you hover the element.
We'd also want a few different types of randomness; numeric ranges are okay, but getting a random value from a list is very useful and handles colors well, too.