CSS random() puts a little deliberate mess back into your layout
Dex Halloran
If your "hand-made" look is a Sass loop spitting out forty :nth-child rules with hard-coded rotations, I've got bad news. That was never randomness. It was a lookup table in a fake mustache. (Everybody shipped one. Mine had a diff you could see from orbit.)
Brecht De Ruyte's new piece in Smashing Magazine is about what replaces it: the native random() function from the CSS Values and Units Level 5 spec. According to his article, Safari landed it first, Chrome 156 is rolling it out right now, and Mozilla has sent positive signals about Firefox. The syntax isn't actually the best part. De Ruyte builds design systems for a living and says so up front ("Consistency is usually our bread and butter as design engineers"), and then he goes looking for the few places where a bit of mess is the right call.
The arguments, one at a time
Here's the grammar he quotes from the spec:
random( <random-caching-key>?, <min>, <max> [, by <step>]? )
In practice he writes plain comma-separated arguments:
rotate: random(-3deg, 3deg);
inline-size: random(20px, 80px, 10px);
animation-duration: random(1.5s, 3s, 0.2s);
The min and max have to share the exact same unit type. random(-4deg, 4deg) is fine, and so is the unitless random(0.3, 1). random(2rem, 100px) is invalid. If you need to mix units, De Ruyte says to put calc() inside the arguments.
The third argument is the step. Without it the browser picks from a continuous range. With it, the value rounds to discrete increments, so top: random(0px, 60px, 15px) only ever lands on 0, 15, 30, 45 or 60px. That's jitter that still respects your grid.
Then there's the caching key, the one piece that needs a real explanation. By default, every random() on a property rolls its own independent value. Randomize inline-size and block-size separately and your "random square" comes out as a random rectangle. Give both declarations the same key and the engine shares the computed value between them:
.card-thumbnail {
inline-size: random(--square-size, 60px, 120px);
block-size: random(--square-size, 60px, 120px);
}
Quick tangent, because this tripped me up. --square-size looks exactly like a custom property, but nothing in that snippet ever declares it. It doesn't hold a value. It's a name the two declarations share. OK, back on track.
Wobbly type, bouncing letters, a fanned-out deck
De Ruyte's first demo splits a word into .char spans and gives each letter a tiny tilt, a nudge and a little ink variance:
.char {
display: inline-block;
rotate: random(-2.8deg, 2.8deg);
translate: random(-1.5px, 1.5px) random(-2.8px, 2.8px);
opacity: random(0.8, 1, 0.02);
}
Hover the word and a .word:hover .char rule resets every letter to straight. Honestly, the first time I watched letterforms wobble with zero JavaScript involved, I felt the way I did the first time :hover worked on something that wasn't a link. A small thing, and it made me grin.
The second demo feeds random() into custom properties (--jump, --dur and a negative --delay), so every letter is already mid-bounce on page load and no two move in sync. The splayed-card demo is the one I'd steal. sibling-index() and sibling-count() compute the fan structure, random() adds jitter on top, and animation-timeline: scroll(root) drives it as you scroll. Math sets the layout, and randomness only nudges it. Then he closes on a punk fanzine called "Never mind the defaults", which, as someone whose teens were scored by drop-D riffs, I respect.
Where it stays out
His no-go list is the part to print out. No randomness in core layouts and grids (column gutters, form field widths, container padding). None in body copy (font sizes, line heights, letter spacing on long paragraphs). And no tilted or shifted buttons and links unless they reset on hover and focus. In his words: "If a user has to chase a moving or crooked button, you've failed them."
There's also the accessibility homework. Split a word into spans and screen readers may read it out letter by letter, so his markup puts aria-label on the h1 and aria-hidden="true" on the wrapper span. Under prefers-reduced-motion: reduce he removes the animation and also flattens rotate and translate. His reasoning is that a lot of random angles can bother people while they scroll, not just the motion.
Using it before every engine has it
Because it's still rolling out, De Ruyte wraps the enhancement in @supports and keeps a straight default:
.badge {
rotate: 0deg;
}
@supports (rotate: random(-2deg, 2deg)) {
.badge {
rotate: random(-2.5deg, 2.5deg);
}
}
He also points to random-item(), which is in the same spec and picks from a list instead of a range (random design tokens, say). He notes that no browser supports it yet.
Verdict: your tokens set the structure, and the engine adds a few degrees of controlled chaos. That's an 8/10, and it only drops two points because one engine still hasn't shipped it.
Source: Smashing Magazine (smashingmagazine.com)