Design systems thrive on consistency, so where does randomness fit in? Through playful demos of organic typography, bouncing letters, and splayed cards, Brecht De Ruyte shows how the new CSS random() function brings a little imperfection to the web without JavaScript libraries or preprocessor loops that generate hundreds of lines of static CSS.
In design, we love spending hours obsessing over consistency, grids, and alignment. But when I heard that the native CSS random() function landed in Safari and is rolling out in Chrome 156, I started wondering: where does randomness actually make sense on the web? Is it something we want in our design system? To find out, I did what I always do in doubt: I built a bunch of playful CodePen demos exploring organic typography, bouncing letters, splayed cards, and a punk fanzine.
As someone who loves building design systems and tinkering with CSS architecture, a big part of my day job revolves around control. We set up spacing scales, lock down typography, set up color tokens, and make sure everything aligns on a grid. Consistency is usually our bread and butter as design engineers and a proud badge of skill when creating design systems. If a card suddenly jumps 20 pixels to the left on a refresh, it’s not creative expression; it’s a bug that the project manager will log.
So when I first heard that the native random() function from the CSS Values and Units Level 5 spec was shipped in Safari, and is currently rolling out in Chrome 156 (with positive signals from Mozilla on Firefox as well), my first reaction was: that’s pretty cool!
And my second reaction was: wait… where would I actually use this without making a mess? How can I convince designers that random might be a good idea for a design system?
Because let’s be honest: why on earth would we want randomness in our stylesheets when we’ve spent the last decade trying to make the web more predictable? Even when using flexbox or fluid typography, we’re still trying to make things as aligned and uniform as possible.
The Charm Of Things Not Being 100% Straight
If you step away from the browser for a second and look at physical things around you, nothing is ever truly uniform.
Take an old letterpress print: the ink bleeds slightly into the paper grain, some letters have a microscopic tilt, and the baseline isn’t always that straight. Or think about a deck of Polaroid photos dropped on a coffee table; those photos aren’t perfect, probably not straight either. Or what about lights flickering in a street at night? That little bit of analog messiness is what makes things feel alive, organic, and human.
On the web, we’ve gotten so good at pixel perfection that sometimes our interfaces end up feeling a bit sterile. We notice this even more now with rapidly created designs by an unguided LLM; we usually end up with the same sort of design we had 10 years ago. Everything is squared off, aligned, neat. It works, but frankly, it does get a bit boring.
Randomness in CSS doesn’t have to mean throwing your design system out the window. It’s about little imperfections and playfulness. Maybe let’s even call it fun.
To see what this actually looks like in practice, I spent a few hours tinkering in CodePen. The demos might feel a bit isolated, but I’m sure that many designers could make the best of this controlled chaos. But before we look at those demos, let’s take a look at the syntax.
How The CSS random() Function Works
The random() function is specified in CSS Values and Units Module Level 5. It generates a pseudo-random value inside your CSS without needing JavaScript or a build-time Sass loop.
The formal specification looks like this:
random( <random-key>?, <min>, <max>, <step>? )
In practice, and across current browser implementations, you can just write comma-separated arguments:
/* Random angle between -3deg and 3deg */
rotate: random(-3deg, 3deg);
/* Random width between 20px and 80px in steps of 10px */
inline-size: random(20px, 80px, 10px);
/* Random animation duration */
animation-duration: random(1.5s, 3s, 0.2s);
Matching Units
Your minimum and maximum bounds must share the exact same unit type. The CSS parser needs to know what dimension it’s dealing with. You can use lengths, angles, time, percentages, or plain unitless numbers:
/* Good: both are angles */
rotate: random(-4deg, 4deg);
/* Good: both are unitless numbers */
opacity: random(0.3, 1);
/* Nope! You cannot mix units */
inline-size: random(2rem, 100px); /* Invalid */
If you ever need to combine units, you’ll have to reach for calc() inside the arguments so each side resolves properly.
Stepping And Rounding
The optional third argument is the step size. If you don’t add it, the browser picks from a continuous range. But if you provide a step, it rounds the random value to discrete increments:
/* Only produces 0px, 15px, 30px, 45px, or 60px */
top: random(0px, 60px, 15px);
This can be helpful in cases where you want things to jump around a bit, but you still want them to land on your pre-defined grid.
Caching Keys: Keeping Things In Sync
Normally, every time random() is evaluated on a property, it creates an independent random value. But there could be an example where you create a box that sets inline-size and block-size at random, but at the same time you need them to match so the element stays a perfect square.
That’s where the caching key comes in:
.card-thumbnail {
/* Both properties share the key --square-size */
inline-size: random(--square-size element-scoped, 60px, 120px);
block-size: random(--square-size element-scoped, 60px, 120px);
}
In the latest spec, a bare dashed ident like --square-size shares its seed globally across every element in the document (which would make all cards identical). To keep width and height linked on a single element while giving each card its own independent size, the spec requires pairing it with the element-scoped keyword (e.g. --square-size element-scoped).
To see this in action, consider a simple example. In the top row, tiles have uncached dimensions, which distorts each tile into an unpredictable, mismatched rectangle. In the bottom row, both inline-size and block-size share --square-size, guaranteeing that each tile remains a pristine 1:1 square.
/* Uncached: width and height resolve independently, creating random rectangles */
.tile-uncached {
inline-size: random(4.2rem, 8.5rem);
block-size: random(4.2rem, 8.5rem);
rotate: random(-3.5deg, 3.5deg);
}
/* Cached: shared key guarantees a clean 1:1 square */
.tile-cached {
inline-size: random(--square-size element-scoped, 4.2rem, 8.5rem);
block-size: random(--square-size element-scoped, 4.2rem, 8.5rem);
rotate: random(--tilt element-scoped, -3.5deg, 3.5deg);
}
Browser Support And Progressive Enhancement
Where can you test this today?
- WebKit / Safari: Safari was the first to land support.
- Chrome / Edge: Shipped and rolling out right now with Chrome 156.
- Firefox: Positive signals on the standards track, so hopefully won’t be too far behind.
Because we’re still in the rollout phase, you’ll want to wrap your experiments in @supports:
/* Baseline for all browsers */
.badge {
rotate: 0deg;
}
/* Enhanced for browsers with random() */
@supports (rotate: random(-2deg, 2deg)) {
.badge {
rotate: random(-2.5deg, 2.5deg);
}
}
The Future random-item() Function
There’s also another function in the spec called random-item(). While random() handles numerical ranges, random-item() lets you pick from a list of predefined tokens or keywords:
/* Picking a random color from your palette */
background-color: random-item(--seed, oklch(75% 0.18 150), oklch(65% 0.22 350), oklch(85% 0.16 85));
This can be handy to randomise from a set of design tokens. This one does not have any browser support at the moment, but it certainly is something to look forward to.
Demo 1: Let’s Just Randomize Letters
When I started playing with the feature, I was also doing some typography tricks with animations, so that’s where my first thought went: what if we just add a tiny bit of imperfection to a word?
Think of vintage book covers. The letters aren’t misaligned by miles, but they have small imperfections. Here is the CSS powering the letters:
@layer demo {
.char {
display: inline-block;
/* A tiny tilt */
rotate: random(-2.8deg, 2.8deg);
/* A micro nudge on X and Y */
translate: random(-1.5px, 1.5px) random(-2.8px, 2.8px);
}
}
The random() function makes it straightforward to introduce this kind of subtle, analog-feeling variation directly in CSS — no JavaScript, no build step, and no manually tweaked per-letter overrides required. Each character gets its own independent random offset, producing the kind of gentle imperfection that gives hand-set type its character.