The creator of the TV show * The Good Place* wrote
a tie-in book about moral philosophywhich includes a chapter called “The Luck of the Draw,” discussing how the
myth of meritocracyleads people to “underestimate the role that luck has played in their lives.” Given how
God seems to play dice with the universe, there is something compelling in the way
art imitates lifewhen websites embrace
controlled chaos in their designs. The jury is out on whether extreme versions of this nondeterminism such as
generative UIare a helpful usage of unpredictable UX. Indeed, when I see the YouTube comments reacting to
Google’s upcoming usage of GenUI in search, maybe it’s taking the idea too far down a bad path. But there is still something about the idea of a webpage that exists in a state of subtle flux each time you land on it, the same way
you can’t step into the same river twice.
Real-world use cases for randomness
I’m a consultant who often works on short-term, greenfield projects, which provide me with a window into the zeitgeist and the trends companies think are the future. It’s no coincidence that the idea of randomness permeated one of my recent projects. That’s epitomized by a burst of confetti to give the user a sense of excitement when they run a random draw they configured. And like many a UI feature in the corporate world, the simple idea of confetti was subject to several revisions to make every randomized particle align with the client’s brand.
In fact, the requirements became custom enough that we ended up ditching the JavaScript plugin we were using and rolled our own confetti implementation! This illustrates the tension between the conflicting needs for chaos and control in UX, even in a fun feature like random confetti.
Wouldn’t it be nice if we could wield controlled presentational randomness in the presentation layer without leaving CSS?
The CSS random()
function emerges
If unpredictable user experiences are having a moment, it follows that CSS will do its part to make randomized layouts easy to implement. The creators of CSS have always been on a mission to harvest common UI patterns into declarative CSS standards. In keeping with that spirit, we see that in late 2025, Safari became the first browser to support the CSS random() spec, as part of an update that emphasized “letting you solve common use cases with HTML and CSS alone,
paving the cowpaths, and reducing the need for JavaScript or third-party frameworks.”
Since then, cool demos and discussions of random()
keep popping up. For instance, Schalk Neethling showed us how CSS random()
can give us fine-grained control over the infamous confetti effect, and Alvaro Montoro made a strong argument that CSS turns out to be the most suitable language for such tasks. He points out this approach is in line with the Rule of Least Power, which encourages “solving a problem using the least powerful language capable of expressing and solving it.”
Now the bad news: half a year after Safari introduced CSS random()
, there isn’t clarity on when it will land in the other browsers. At time of writing, there are signs of life that both Chrome and Firefox have been working on it, but no guarantees about when we will be able to use it outside of the Apple world, even behind a browser flag.
So, it seems currently I can only try the online demos of CSS random()
on my work MacBook and not on my PC where I do my personal projects. I am tempted to write my own implementation, but the syntax is surprisingly intricate, mostly because of elaborate random caching and keying semantics, combined with the options for base values and intervals. Even if I could manage to get all those details correct, CSS random()
is part of an editor’s draft spec that’s in the “early exploration phase” and “major breaking changes are expected.”
On top of that, from my dive into CSS polyfills in my article on ::nth-letter, we know the whole idea of a CSS polyfill can be a minefield.
With all these obstacles in mind, a person would have to be a special breed of crazy to attempt to polyfill CSS random()
.
Let’s polyfill CSS random()
One of the commenters on a neat YouTube demo of the feature marvelled that it’s a “feature that works ONLY IN SAFARI?!? Did the Earth get flipped upside down?” Indeed, I am more accustomed to getting my first opportunity to experience emergent features in Chrome, which means my friends on iPhones often can’t run my experiments.
And yet, in the case of random()
, it’s darkly poetic that a feature based on chance appears in an unexpected place where many of us can’t use it. In fact, even Safari users may benefit from my css-random-polyfill package, because
Safari updates are tied to the OS, meaning not everyone can upgrade to the latest version of the browser. Besides, we know how much Apple loves it when you
hack their stuff to improve compatibility.
Jokes aside, Apple seems serious about the “hackability” and transparency of everything about the open source WebKit engine that powers Safari, and most of the demos I’ve used to test my polyfill are forks of demos from the WebKit blog, in which the Apple Safari team showed off the possibilities for CSS random() back when it was in Safari preview.
Demo: Random starfield
Here’s my cross-browser version of the first demo from the Safari team’s article. It’s a randomly scattered field of stars fading in and out at random intervals. The larger, four-pointed stars all tilt at the same randomly selected angle. All stars have subtle, randomly hued shadows around them.
To migrate the Safari-only original to a version that works in Chrome and Firefox, we need to change the HTML to reference my polyfill script and add the randomized
marker class to all elements that we want to polyfill.
<!-- the script processes usages of css random on page load -->
<script src="https://unpkg.com/css-random-polyfill@latest/dist/css-random-polyfill.js"></script>
<!-- 200 star divs, we add the "randomized" marker class so css-random-polyfill knows which elements to target -->
<div class="randomized star"></div>
<div class="randomized star"></div>
<!-- etc. -->
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
As for the CSS, unlike my :nth-letter polyfill which uses a nonstandard selector that has to be
translated into valid CSS at runtime— and introduces
drawbacks in the process— this time we need to support a new
functionin CSS instead of a new
selector. It turns out the CSS we can use in this situation is technically valid, even in browsers that have never heard of CSS
random()
. More later on why it is valid, but for now, just notice that anywhere we want a random value, we store it in an intermediate custom property, and we always have to follow the convention that the property name starts with the prefix --random
.
.star {
--random-star-size: random(1px, 7px, 1px);
background-color: white;
border-radius: 50%;
aspect-ratio: 1/1;
width: var(--random-star-size);
position: fixed;
--random-top: random(0%, 100%);
--random-left: random(0%, 100%);
top: var(--random-top);
left: var(--random-left);
--random-hue: random(0, 360);
filter: drop-shadow(0px 0px calc(var(--random-star-size) * 0.7) oklch(0.7 0.2 var(--random-hue)))
drop-shadow(0px 0px calc(var(--random-star-size) * 3) white);
mix-blend-mode: hard-light;
--random-speed: random(2s, 5s);
animation: fade-in var(--random-speed);
animation-iteration-count: infinite;
--random-delay: random(2s, 5s);
animation-delay: var(--random-delay);
animation-direction: normal;
}
This starfield demo showcases a few different variations of the supported random()
syntax, such as the optional third argument for specifying a step interval which, in this case, is used to randomly select only whole number values within the range:
--random-star-size: random(1px, 7px, 1px);
…and the element-shared
base value, which we use here to tilt every four-pointed star by the same randomly selected angle.
.star.fourpointed {
--random-rotation: random(element-shared, -45deg, 45deg);
rotate: var(--random-rotation);
}
Note: In the original starfield demo, most of the random values were used inline, which is admittedly more elegant. The spec that includes random()
makes it clear that this kind of function “can be used in place of any part of any property’s value,” just like calc()
or min()
. So, by requiring extra ceremony and conventions, the polyfill is supporting a subset of what we will get with native random()
. To see the glass half-full, it means the CSS stays compatible with the native implementation: we could delete the script reference to the polyfill once native support goes baseline and our code will still work, like it does today when it detects native support in Safari. in this case the polyfill does not process random()
calls at all and it lets Safari do all the work. This is a compromise I can live with, especially if the alternative is to press our noses against the glass of Safari-only demos on YouTube and make comments such as one viewer did: “Can’t wait to use this in prod in 4 years.”
Demo: Random Colored Grid Cells
Chris Coyier said of the original starfield demo from Apple that he found it “pretty darn compelling!” I agree, and when I was testing my polyfill, that demo was fun to watch randomly twinkling, refresh and see the stars scatter differently using an emergent, declarative CSS standard. By contrast, I can’t say I have ever sat around wishing I could create a 100×100 CSS grid with randomly multicolored cells, so this example from the Safari team feels a bit like a contrived excuse to randomize something. However, it did help me test the polyfill support of a few different variations of the syntax.
The polyfill allows for some flexible syntax. You can see that references to custom properties passed to the random()
function get substituted as expected, and you can see that inlining multiple random()
calls in the same value works. For example, we can create a grid-area shorthand property value with randomized
row-start
and column-start
values.
.rectangle {
--random-grid-area: random(1, var(--rows), 1) / random(1, var(--columns), 1);
grid-area: var(--random-grid-area);
}
Demo: Wheel of fortune
This example is from Tim Nguyen from the Safari team. To continue the themes of chance and synchronicity, I’ll mention that I had the good fortune to meet Tim last year when I spoke at Web Directions 2025. My talk came right after his talk, and now that I’m forking his CSS random()
demo to create a cross-browser version, he is once again a tough act to follow.
You can see in this example that the final random position of the wheel uses a different unit for its step interval parameter than for the minimum and maximum parameters.
@keyframes spin {
from {
rotate: 0deg;
}
to {
rotate: var(--random-rotation);
}
}
#wheel {
--random-rotation: random(2turn, 10turn, 20deg);
}
The mix of types is supported because the specs say the values must be “resolvable to the same data type,” so we are able to mix units as long as they are in the same “overall data type,” such as turn
and deg
, familiar from the way CSS calc()
adds values with different units when it makes sense, using CSS typed arithmetic.
Note: To make the demo work with the polyfill, I had to define the variable in a CSS class that will be applied when the polyfill first loads, in contrast to Tim’s original demo which uses the random()
function inside a keyframes animation that was applied based on a checkbox hack. That’s because, for now, the polyfill only processes the computed styles that are applied to elements when the page first loads. Since all my tests pass with this implementation, I am leaving it like that for now in the interest of doing the simplest thing that could possibly work. There are ways we could explore to make the polyfill react to dynamic changes to the computed styles and/or the DOM.
Demo: Random squares
Chris Coyier has a knack for writing code that’s either as tricky or as simple as needed to get his point across, and his CodePen “Very basic random() in CSS” is maybe the simplest demo of CSS random()
possible, showing three randomly positioned squares with random colors. Below is my cross-browser version, which I also modified to randomize the size of the squares, as a test that my polyfill supports random value sharing using custom keys.
Here is the code I added to make each square have a random height that is equal to its random width:
--random-height: random(--side, 40px, 100px);
--random-width: random(--side, 40px, 100px);
width: var(--random-height);
height: var(--random-width);
This reassures that we are supporting the correct syntax. Admittedly, custom keys will be more useful in the real native version, which won’t need the intermediate variables. Since we are using intermediate custom properties, we could just have used one custom property named --side
and referenced that for both the height
and width
values.
Chromium-only bonus demo: Simulate random-item
using a custom CSS function
Many of the above demos include random colors. That’s achieved by passing random numeric values into CSS color functions such as rgb() or
lch()
, although no browser currently implements it (except for experimental
random-item()
functionsupportin safari preview). If we had this function, we could select a random color or anything else from an arbitrary list of values:
random-item(element-shared, red, blue, green);
The random-item
function takes a mandatory first argument of the type random-caching-options, the same as CSS
random()
, but then it takes a variable length list of arguments to randomly select from, rather than a minimum and maximum value.I don’t feel like complicating the polyfill to support a CSS syntax that isn’t implemented in any browser — evidently I only give myself permission to do that once a year. But now that we have a version of CSS random()
in Chromium which also supports CSS custom functions and inline conditionals, it’s hard to resist seeing what happens if we combine all these weird and wonderful things into one experiment. It turns out these features together can get us pretty darn close to the functionality we’d get from random-item()
.
--random-index: random(element-shared, 1, 5, 1);
--random-color: --item(var(--random-index), aqua, purple, pink, grey, green);
If you’re using a Chromium-based browser, you can see the code in action in this version of the squares demo which sets all three elements to the same color randomly selected from the list.
The implementation of my generic --item
custom CSS function takes an --index
argument followed by 10 optional arguments. These could be increased to any number of arguments you think will be the realistic maximum size of a collection you would need. Each of the optional arguments is made optional by defaulting it to an empty value, so the caller of the function only needs to pass in the arguments it needs to index. Lastly, the function maps the --index
to the argument at that index, because CSS custom functions do not support variable length collections of arguments the way JavaScript functions do.
@function --item(--index,
--arg-1: ,
--arg-2: ,
--arg-3: ,
--arg-4: ,
--arg-5: ,
--arg-6: ,
--arg-7: ,
--arg-8: ,
--arg-9: ,
--arg-10: ) {
result: if(
style(--index: 1): var(--arg-1);
style(--index: 2): var(--arg-2);
style(--index: 3): var(--arg-3);
style(--index: 4): var(--arg-4);
style(--index: 5): var(--arg-5);
style(--index: 6): var(--arg-6);
style(--index: 7): var(--arg-7);
style(--index: 8): var(--arg-8);
style(--index: 9): var(--arg-9);
else: var(--arg-10);
);
}
Sidenote: This generic helper function is interesting, because Temani Afif has demonstrated cool use cases for being able to choose from a list of colors using an --index
variable, but the solution he created was specific to the color data type and he freely admits it’s “more of a hack than a CSS feature. So, use it cautiously.” By contrast, the custom function approach will work with a list of any data type, and I wouldn’t describe it as a hack because it’s using CSS standards as intended, albeit emergent standards that aren’t available in all browsers just yet.
How the polyfill works
Now we have gained confidence in our random()
polyfill, you might be curious how it works. Is this a good time to level with you and say I don’t fully know? That’s a very 2026 predicament, but thankfully it’s not because of AI.
As I hinted at the start, my level of eagerness to use new CSS syntax before it’s supported is matched only by my level of laziness to implement and maintain my own version of random()
, so I went hunting for an open source JavaScript implementation and was pleasantly surprised it exists!
As you might expect, it’s not designed for the exact purpose I want it for. it’s in an implementation that’s designed to be used at build-time rather than on the client, as a PostCSS plugin. Digging through the source we see that this plugin wraps the MIT-licensed @csstools/css-calc
which has no dependencies and isn’t coupled to PostCSS. The Readme for this package says it only implements the older CSS Values and Units Module Level 4, but we see from the commit history that it’s recently had an “update to latest spec” of random()
and we see it passing automated tests for the kind of random goodness we have been enjoying in this article.
My main question is how on earth we are going to hook it up to client-side CSS, but it turns out not to be too much custom code:
import { calc } from "@csstools/css-calc";
const calcFn = calc;
if (!CSS.supports("width", "random(0px, 100px)")) {
const styleTag = document.createElement("style");
styleTag.textContent = ".randomized { display: none; }";
document.head.appendChild(styleTag);
const elementIDs = new WeakMap();
const documentID = crypto.randomUUID();
document.querySelectorAll(".randomized").forEach((element) => {
const styles = getComputedStyle(element);
[...styles]
.filter((property) => property.startsWith("--random"))
.forEach((propertyName) => {
const css = styles.getPropertyValue(propertyName);
const value = resolveRandom(css, {
element,
propertyName,
documentID,
elementIDs,
calcFn,
crypto,
});
element.style.setProperty(propertyName, value);
});
});
if (styleTag.parentNode) {
styleTag.parentNode.removeChild(styleTag);
}
}
function resolveRandom(css, { element, propertyName, documentID, elementIDs, calcFn, crypto }) {
const patchedCss = css.replace(
/random\(\s*(?!(?:[^,]*\b(?:shared|scoped)\b|fixed\b|--))([^,]+),/gi,
(_, expression) => `random(fixed ${Math.random()}, ${expression},`
);
return calcFn(patchedCss, {
precision: 5,
toCanonicalUnits: true,
randomCaching: {
documentID,
elementID: elementIDs.getOrInsert(element, `element-${crypto.randomUUID()}`),
propertyName,
},
});
}
Let’s translate this code into natural language steps:
- If we detect that the browser supports native CSS
random()
, then the polyfill will do nothing and let the browser handle any calls in CSS torandom()
. - If it doesn’t support the feature, we temporarily hide all elements marked as
.randomized
to prevent a flicker. - We loop through all the
--random
prefixed properties in any element that has the.randomized
CSS class. - For each
--random
custom property, we take advantage of thefactthat the “allowed syntax forcustom propertiesis extremely permissive,” which means that even if the CSS parser does not understand an expression used in the value for a property such as--random-grid-area: random(1, var(--rows), 1) / random(1, var(--columns), 1)
, the value will be parsed into a string which can “be read and acted on by JavaScript.” The browser will also resolve any calls tovar()
and substitute those into the computed value, regardless of any surrounding gibberish it can’t interpret. - We generate unique surrogate identifiers for the document and each randomized element we pass to
@csstools/css-calc
together with the expression string that contains each usage ofrandom()
. This allows CSS Tools to respect the random caching rules such aselement-shared
. - If no base is specified in a usage of
random()
, the library doesn’t seem to generate evenly distributed values (for example, the stars in the first test kept ending up in weird clusters), so we break out the proverbialduct tapeand patch the problem by injecting a fixed randomly generated base value if the user didn’t provide one. - Using the value we get back from
@csstools/css-calc
interpreting therandom()
call, we set the property to that value with an inline style on the randomized element. - We remove the class declaration we injected to hide the randomized elements while we were resolving them.
Point 4 is a big deal. Interpreting arbitrary custom property values using CSS is the closest we have in present day CSS to an honest-to-goodness documented extension point for the language. Since arbitrary expressions in custom variable values are valid and can be read by JavaScript via the computed styles, this approach has the potential to avoid many of the known downsides of polyfilling CSS such as refetching and rewriting stylesheets, doing our own parsing of CSS, and other fun but dangerous pastimes.
Random parting thoughts
Fittingly, it’s only by good luck that an open source project has already done most of the work we need to be able to run CSS random()
in any browser while we wait for native support. A lot of people claim they can’t wait for this feature to be available in more browsers, so it will be interesting to see whether people choose to wait now that a polyfill exists. Seeing Chris Coyier’s reaction to the starfield demo, his enthusiasm was contagious! I had a similar moment when I first got the demo working in other browsers. Let me know if having this polyfill available sparks creativity for your own projects. I definitely have ideas for some more advanced use cases for it, which is what prompted me to polyfill it.
Till next time, happy randomizing from your friendly neighbourhood random guy.