Skip to content

How CSS random() Changes the Way You Stagger Motion

Safari Technology Preview 251 added CSS random() and random-item(). Here is what native randomness does for staggered motion, and what it cannot do.

· · 10 min read
A pile of multicoloured paper confetti squares on a white table

Apple shipped a preview build on August 26, 2026, and buried in the CSS section was a line most people scrolled past: support for the random-item() function, plus random() inside custom properties. That is the first browser implementation of native randomness in CSS, and for anyone who animates things it is a bigger deal than it looks.

TL;DR: Safari Technology Preview 251 added random() support in custom properties, two new caching keywords, and random-item(). No stable browser ships any of it. The feature that matters is not randomness, it is the per-element cache that makes a random value stable across repaints (WebKit, 2026).

Why a Preview Build Is Worth Reading

Preview builds are where you find out what the next two years of layout look like. Safari Technology Preview 251 covers WebKit revisions 317935 through 319386 and lists a dozen new CSS features, including object-view-box, the corner-shape property, ident(), inherit(), comma-separated conditions in @container queries, and @supports at-rule(...).

That is a lot for one preview. But the random functions are the ones that change how a motion designer would write a stylesheet, so that is where I want to spend the words.

The Problem CSS Has Never Solved

Take a wall of forty cards that should fade in. You want them to arrive at slightly different moments, because forty things arriving in perfect lockstep reads as a machine, not a page.

Today you have three options and all three are bad.

The first is a block of nth-child rules with hand-written delays. It works, it is honest, and it is forty lines of CSS that a designer has to regenerate every time the card count changes. I have written this block more times than I want to admit.

The second is to have your framework write inline style="--i: 17" on every element and read it back in CSS. This works well and it is what most component libraries do, but it means your visual rhythm now depends on your rendering layer, which is a strange place for it to live.

The third is JavaScript that measures the DOM and assigns delays. It is the most flexible and the most expensive, because now your fade-in cannot start until a script has run.

None of these are randomness. They are all sequences. What CSS has never had is a way to say "give this element a number, any number in this range, and keep giving it the same number."

A white table covered with lots of small squares of coloured paper
Photo by Kier in Sight Archives on Unsplash

What the Syntax Looks Like

The shape of random() in the CSS Values and Units Level 5 draft is a range with an optional step:

.leaf {
  animation-delay: random(0ms, 600ms, by 50ms);
  rotate: random(-8deg, 8deg);
}

The by keyword snaps results to increments, so that first line produces one of thirteen values rather than any float in the range. That matters more than it sounds. Snapped delays keep a stagger feeling composed instead of smeared, and they let you reason about the worst case: nothing arrives later than 600ms, full stop.

random-item() is the discrete cousin. You hand it a cache key and a list, and it picks one:

.chip {
  font-family: random-item(--x, serif, sans-serif, monospace);
}

The draft also allows curly braces to group values that contain their own commas, so random-item(--x, {Times, serif}, {Arial, sans-serif}) picks between two complete font stacks rather than four individual families. Small detail, and exactly the kind of thing that would otherwise generate three years of Stack Overflow questions.

For motion the discrete version is arguably the more useful one. Randomising a delay across a continuous range gives you noise. Randomising which of three easing curves an element uses gives you character, because you chose all three. If you are still deciding where the line between CSS and a library sits, CSS vs GSAP in 2026 is the longer argument.

The Caching Is the Actual Feature

Here is the part that write-ups keep burying, and it is the part I would put on the first slide.

A random value in CSS is useless unless it holds still. If the browser re-rolled on every style recalculation, every hover on an unrelated element could reshuffle your entire grid. So the specification puts caching in the function signature itself, ahead of the range, as <random-caching-options>.

Two behaviours matter to a designer. A dashed identifier such as --x ties results together: two declarations that share a key get the same roll, which is how you make an element's delay and its rotation agree instead of fighting. And a per-element option gives each element its own value, which is the thing that replaces the forty nth-child rules.

Safari Technology Preview 251's specific addition here was support for property-scoped and property-index-scoped caching keywords, alongside random() working inside custom properties. Those keyword names are worth flagging as unstable: the Working Group has moved this part of the grammar around more than once, and a public archive thread from February 2025 was still arguing about whether the min, max and step arguments belong in the caching key at all. Write demos, not documentation.

Randomness that re-rolls is a bug. Randomness that holds still is a design tool.

What This Does and Does Not Replace

Let me be concrete about where I would actually reach for it.

Good candidates. Ambient background particles. Confetti and celebration bursts. A grid of avatars settling into place. Falling leaves, snow, bubbles, anything standing in for physical variation that no human choreographed. Subtle rotation on a stack of "paper" cards so the pile looks handled rather than printed. Per-element wobble amplitude on a hover state, so a row of icons does not move as one rigid object.

Bad candidates. Anything the reader is meant to follow. A stepped onboarding flow, a form's field-by-field reveal, a data visualisation that builds in a meaningful order. If someone could reasonably be asked "what happened first?", randomness has taken away their answer. That test is the same one I use for micro-interactions, and it fails the same way for the same reason.

There is a third category I keep going back and forth on: list items in a feed. A random 0 to 200ms stagger does look more organic than a strict cascade. It also means the third item can beat the first, which on a feed sorted by importance is a small lie told in motion. I lean against it. Reasonable people disagree, and I have shipped it both ways.

Crumpled white paper printed with a faint grid pattern
Photo by Juliette Jardin on Unsplash

The Thirty-Two Lines, Before and After

It is worth seeing the trade at full size, because the saving is not subtle.

Here is the shape almost every card grid ends up with. I have trimmed it to six items; the real ones run to twenty or forty.

.card { animation: rise 320ms ease-out both; }
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 40ms; }
.card:nth-child(3) { animation-delay: 80ms; }
.card:nth-child(4) { animation-delay: 120ms; }
.card:nth-child(5) { animation-delay: 160ms; }
.card:nth-child(6) { animation-delay: 200ms; }

Three things are wrong with it and only one is the line count. It caps out: card seven and everything after it fire at zero, so a grid that grows silently loses its stagger from the seventh item on. It is order-locked: reverse the sort and the animation still cascades top-left to bottom-right, which now contradicts the content. And it is unreviewable, because the delays are data pretending to be code.

The replacement is a declaration:

.card {
  animation: rise 320ms ease-out both;
  animation-delay: random(--stagger, 0ms, 200ms, by 40ms);
}

Same six possible delays, no cap, no order lock, and the shared --stagger key means a second property can ride the same roll:

.card {
  animation-delay: random(--stagger, 0ms, 200ms, by 40ms);
  rotate: random(--stagger, -1deg, 1deg);
}

Cards that arrive late lean the same way every time they arrive late. That correlation is free here and genuinely fiddly to arrange any other way.

The Accessibility Question Nobody Asked

prefers-reduced-motion does not know what random() is, and it should not have to. But a randomised system has a failure mode a hand-written stagger does not: you cannot fully enumerate what a user will see.

With forty nth-child rules I can tell you the exact worst case. With a random delay range I can tell you the bounds, which is nearly as good, and with by I can tell you the exact set of possible values, which is better. That is a real argument for always writing the step, even when a continuous range would look marginally smoother. Bounded and enumerable beats smooth when someone has to sign off on it.

The other half is unchanged and non-negotiable: wrap the whole thing so that a reduced-motion user gets the end state immediately. Randomness inside a motion block does not change the rule, it just makes forgetting the rule more expensive, because the thing you forgot is now unpredictable.

.leaf {
  animation-delay: random(0ms, 600ms, by 50ms);
}

@media (prefers-reduced-motion: reduce) {
  .leaf {
    animation: none;
  }
}

What I Would Do This Week

Not ship it. That should go without saying, but the gap between "a preview build supports it" and "your users have it" is measured in years, not months. linear() landed in Safari 17.2 in December 2023 and people were still writing "can I use this yet" posts about it eighteen months later.

What is worth doing now is auditing. Go find the places in your current stylesheets where you faked per-element variation, and mark them. In the two codebases I checked while writing this, the answer was one confetti component, one avatar stack, and a nth-child block thirty-two lines long that nobody had touched since the card grid changed from 12 items to 18. That block had been wrong for months and nothing had flagged it, because a stagger that runs out halfway just looks like a slightly duller animation.

That is the honest case for caring about a preview-build feature: not the demo you will build, but the maintenance debt you can now see clearly, because you finally know what the right shape of the solution looks like.

Where This Sits Against the Rest of 2026

Chrome 150 brought its own batch of motion changes earlier this summer, and Safari 26.6 in July was a pure repair release with three CSS fixes that quietly changed what shipped stylesheets did. Reading them side by side, the pattern for 2026 is clear enough: the interesting motion work is moving out of JavaScript and into declarations, one function at a time.

random() is the most designer-shaped of those functions so far. It does not make anything faster and it does not enable anything that was impossible. It removes a category of code that exists purely because CSS could not vary a number per element, and that code was always the ugliest part of any animation I have written.

Would I trade random() for real spring physics in CSS? Probably. But nobody is offering that deal, and this one is already in a build I can download.

Frequently Asked Questions

Can I use CSS random() in production yet?
No. As of September 2026 no stable browser ships it. Safari Technology Preview 251 is a preview build that Apple distributes separately from Safari and that nobody's audience is running. Treat this the way you would treat any Working Draft feature: build a demo, form an opinion, and put a progressive-enhancement wrapper around it if you ship anything at all. The useful move today is deciding what you would do with it, because the answer changes what you build with JavaScript in the meantime.
Does random() re-roll on every frame or every repaint?
It is not supposed to. The whole point of the caching options in the syntax is that a random value is generated once and then held, so a repaint, a resize or a style recalculation gives you the same number. Without that guarantee the function would be unusable for animation, because every element would flicker as the browser re-evaluated it. Caching is the feature. The random part is almost incidental.
What is the difference between random() and random-item()?
random() gives you a number, a length, a percentage or an angle inside a range, optionally snapped to a step with the by keyword. random-item() picks one entry from a list you write out. Use random() for anything continuous, like a delay between 0 and 400ms or a rotation between -6deg and 6deg. Use random-item() for anything discrete, like choosing between three easing curves or three named colours, where the in-between values would be meaningless or off-brand.
Will this replace nth-child stagger hacks?
For a specific class of them, yes, and good riddance. The pattern where you write out twenty nth-child rules with hand-incremented delays exists because CSS had no way to vary a value per element. Once random() and the per-element cache land in stable browsers, that whole block collapses into one declaration. What it will not replace is deliberate sequencing, because a stagger that follows reading order is not random and should never be.
Is randomised motion a good idea in the first place?
Sometimes, and less often than people think. Random works when it stands in for physical variation nobody choreographed: leaves, confetti, particles, a crowd of avatars settling in. It fails when the reader is meant to follow something, because unpredictable timing removes the thread they are following. My rule is that if a viewer could ever be asked what happened first, the motion is not a candidate for randomness.