Embracing Playful Imperfection With The CSS `random()` Function

Wait 5 sec.

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% StraightIf 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 WorksThe 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( ?, , [, by ]? )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 UnitsIt won’t be a shocker for most people, but I thought I should at least mention that 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 RoundingThe 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 SyncNormally, 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, 60px, 120px); block-size: random(--square-size, 60px, 120px);}Because both declarations reference --square-size on that element, the browser engine shares the same computed value between them.To see this in action, I created a little example. In the top row, the 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. Refresh the CodePen a couple of times and notice the bottom cards keeping the same randomness:See the Pen Uncached vs cached random() [forked] by @utilitybend./* 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, 4.2rem, 8.5rem); block-size: random(--square-size, 4.2rem, 8.5rem); rotate: random(--tilt, -3.5deg, 3.5deg);}Browser Support And Progressive EnhancementWhere 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 FunctionThere’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.And now for the practical part.Demo 1: Let’s just randomize lettersWhen I started playing with the feature, I was also doing some typography tricks with animations. So that’s where my first thought went: Typography! 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.See the Pen Imperfection random() [forked] by @utilitybend.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); /* Very subtle ink variance */ opacity: random(0.8, 1, 0.02); transition-property: rotate, translate, opacity; transition-duration: 0.3s; transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); } /* Snap straight on hover */ .word:hover .char { rotate: 0deg; translate: 0 0; opacity: 1; }}I also added a little hover effect. While you’re reading, the word feels playful. But the second you hover over it, every character snaps back.Please Don’t Break The Screen ReaderOne quick accessibility warning here: If you break a word into separate elements so you can style each letter, screen readers will often read it out as individual characters: “i - m - p - e - r - f - e - c - t - i - o - n”, or pause awkwardly between each one. That’s a terrible user experience.Always hide the visual spans from assistive tech and provide the full word: i m p e r f e c t i o n Screen reader users get the clean text, and sighted users get the fun visual quirks.Demo 2: Animating The Letters: Pure Procedural BounceOnce I had those letters sitting there with their little tilts, I couldn’t resist: what if we animated it?I started tinkering with a bounce keyframe. I was just goofing around, but the end result gave me a little smile. If you animate every letter with the exact same timing, you get that clean sine wave we’ve all seen before. But if every letter gets its own jump height, its own duration, and its own landing squash, suddenly the word looks like a bunch of little characters jumping around on a trampoline.See the Pen Bouncy imperfection with CSS random() [forked] by @utilitybend.Here is how the bounce parameters are wired up:@layer demo { .char { display: inline-block; transform-origin: center bottom; /* Procedural bounce variables */ --jump: random(-35px, -55px, 2px); --tilt: random(-8deg, 8deg, 0.5deg); --dur: random(1s, 1.2s, 0.1s); --delay: random(-3s, 0s, 0.1s); --rebound-squash: random(1.08, 1.18, 0.02); animation: letter-bounce var(--dur) linear var(--delay) infinite; } @keyframes letter-bounce { /* apex */ 0% { translate: 0 var(--jump); rotate: var(--tilt); scale: 1 1; animation-timing-function: cubic-bezier(0.65, 0.05, 0.85, 0.35); } /* touchdown */ 44% { translate: 0 0; rotate: calc(var(--tilt) * 0.2); scale: 0.96 1.05; animation-timing-function: ease-out; } /* squash */ 52% { translate: 0 0; rotate: 0deg; scale: var(--rebound-squash) calc(2 - var(--rebound-squash)); animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1); } /* back up */ 62% { translate: 0 calc(var(--jump) * 0.2); rotate: calc(var(--tilt) * -0.4); scale: 0.98 1.03; animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1); } 100% { translate: 0 var(--jump); rotate: var(--tilt); scale: 1 1; } }}Notice the negative animation delay (--delay: random(-3s, 0s)).If you set a positive delay, the letters sit frozen on page load, waiting for their timers to expire before jumping. By using a negative delay, the browser pretends the animation has already been running for a few seconds. The moment the page loads, every letter is already bouncing at its own rhythm.Demo 3: Tilting Cards And Dynamic ShadowsWe’ve built cards. Lots of cards. So many cards... Portfolio grids, blog summaries, feature lists. And most of the time, they sit in three or four neat, boring columns.For dashboards, that’s exactly what you want. But if you’re building a lifestyle portfolio, a children’s website, or a collection of vacation photos, we can be a bit more playful. In the following example, I did just that:See the Pen Random card tilt [forked] by @utilitybend.Here’s how I put that together:@layer demo { .card { /* A gentle random tilt and vertical shift */ --tilt: random(-3.5deg, 3.5deg); --shift-y: random(-6px, 6px); rotate: var(--tilt); translate: 0 var(--shift-y); /* Shadow follows the tilt angle! */ box-shadow: calc(var(--tilt) * 1.5) calc(10px + abs(var(--tilt) * 1px)) 28px oklch(20% 0.02 260 / 0.07); transition: rotate 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), translate 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.4s ease; /* Straighten up on hover or keyboard focus */ &:hover, &:has(.button:focus-visible) { rotate: 0deg; translate: 0 -10px; z-index: 10; box-shadow: 0 20px 44px oklch(20% 0.02 260 / 0.12), 0 2px 6px oklch(20% 0.02 260 / 0.04); } }}In the example, playing around with the shadow was a fun experiment. I will probably try more effects with that in the future.Demo 4: The Splayed Deck With sibling-index and scrollHere is where modern CSS features start getting really interesting. In the previous demo with tilting cards, we used pure randomness. But what if you have a stack of cards that needs to fan out across the screen as you scroll?In this example, I decided to use math for the structure and randomness for the human imperfection.See the Pen Scroll drivin animations with random() tilted cards [forked] by @utilitybend.Here is the setup:@layer demo { .deck-card { position: absolute; /* Mathematical fan base via sibling-index() (centered around middle card) */ --elevation: sibling-index(); --base-rot: calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg); --base-x: calc((sibling-index() - (sibling-count() + 1) / 2) * 60px); --jitter-rot: random(-3.5deg, 3.5deg); --jitter-y: random(-12px, 12px); --jitter-x: random(-8px, 8px); transform-origin: center 130%; /* SDA */ animation: splay-fan linear both; animation-timeline: scroll(root); animation-range: 0% 80%; transition: scale 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); &:hover { scale: 1.05; /* Hovered item on top */ z-index: calc(sibling-count() + 1); } } @keyframes splay-fan { from { rotate: 0deg; translate: 0 0; box-shadow: 0 1px 3px oklch(20% 0.02 260 / 0.1), 0 2px 8px oklch(20% 0.02 260 / 0.07); } to { rotate: calc(var(--base-rot) + var(--jitter-rot)); translate: calc(var(--base-x) + var(--jitter-x)) var(--jitter-y); /* Splayed: 1. Sharp inter-card contact edge shadow 2. Elevated atmospheric shadow scaled realistically by sibling-index() */ box-shadow: 0 calc(1px + var(--elevation) * 0.6px) 4px oklch(20% 0.02 260 / 0.1), calc(var(--base-x) * 0.15) calc(10px + var(--elevation) * 5px) calc(18px + var(--elevation) * 7px) oklch(20% 0.02 260 / 0.13); } }}Look at how cleanly this works:sibling-index() calculates the mathematical fan so cards spread out evenly around the center card, and scales --elevation so cards higher in the stack cast wider shadows.calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg); makes sure the amount of cards doesn’t matter; they each will rotate the same 12deg amount.random() adds the slight wobble you'd get if a person dealt the cards by hand.animation-timeline: scroll(root) ties the entire fan and shadow transition to the scrollbar.Demo 5: Random VelocityIf you’ve ever looked at brutalist or high-end fashion websites, you’ve probably seen horizontal marquee text drifting across the screen as you scroll down the page.Usually, you see developers creating classes like .drift-slow, .drift-fast, .drift-reverse, or using a hefty JavaScript scroll listener to compute transforms.With random() and a tiny bit of math, we can get procedural drift speeds in just a few lines:See the Pen Random() velocity scroll animation [forked] by @utilitybend.Here is the CSS:@layer demo { .drift-line { white-space: nowrap; /* Alternate direction between rows using math parity */ --direction: calc(pow(-1, sibling-index())); /* Velocity distance */ --travel: random(15%, 44%); /* Tied to page scroll */ animation: tectonic-glide linear both; animation-timeline: scroll(root); animation-range: entry 0% cover 100%; } @keyframes tectonic-glide { from { translate: calc(var(--direction) * -1 * var(--travel)) 0; } to { translate: calc(var(--direction) * var(--travel)) 0; } }}calc(pow(-1, sibling-index())) gives us alternating directions (1, -1, 1, -1), and random(15%, 44%) decides how fast each row travels. As you scroll, each typographic line moves at its own tempo.Demo 6: Blurry RandomnessI love photography, and one of my favorite things about prime lenses is shallow depth of field. When you shoot at a wide aperture like f/1.7, only whatever is right on your focal plane stays sharp. Everything else melts away into soft bokeh. Yep, photography is all about that bokeh!By combining random() with filter: blur(), opacity, and scale, we can recreate that lens feel:See the Pen All about that random() bokeh [forked] by @utilitybend.Here’s the snippet:@layer demo { .focal-item { /* Random optical blur, opacity, and scale */ filter: blur(random(0px, 8px, 1px)); opacity: random(0.25, 1, 0.05); scale: random(0.85, 1.25, 0.05); transition: filter 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s ease, scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* Rack-focus on hover */ &:hover, &:focus { filter: blur(0px); opacity: 1; scale: 1.15; } }}Some words land at blur(0px) with full opacity right in front of your eyes. Others blur out to 6px or 8px and fade into the background. And when you hover over any blurry item in the back, it rack-focuses right into sharpness. It’s a great little effect for decorative hero sections or tag clouds.Where Randomness Makes Sense (And Where It Doesn’t)Now that we’ve played with the wild side, let’s talk about where this actually belongs in our daily work.Good Use CasesSubtle micro-imperfection on decorative UI: Tilting polaroids, slight angles on sticky notes, or typographic hero titles.Desynchronizing ambient loops: Status beacons, gentle floating badges, or background particle grids where perfect unison looks robotic.Generative backgrounds: Star fields, confetti celebrations, floating bokeh circles, or texture grain.Editorial art direction: Magazine spreads, event flyers, brutalist layouts, and playful landing pages.Layering on top of math: Using sibling-index() for the structural grid and adding small random offsets for human warmth.Where You Should Avoid ItCore layouts and grids: Don’t randomize column gutters, form field widths, or container padding. People need predictable layout geometry to navigate.Body copy: Don’t touch font sizes, line heights, or letter spacing on long-form paragraphs. Reading fatigue is real.Interactive targets: Don’t leave small buttons or links tilted or shifted without resetting them on hover and focus. If a user has to chase a moving or crooked button, you’ve failed them.But! I am happy to be proven wrong here. I encourage you to challenge the status quo and experiment with this.Always Respect prefers-reduced-motionAny time you use animations, you have to respect user preferences for reduced motion:@media (prefers-reduced-motion: reduce) { .char { animation: none; rotate: 0deg; translate: 0 0; }}If someone prefers reduced motion, it could also be a good idea to flatten the angles, not just the animations. I can imagine that a lot of random angles can also upset people while scrolling. Design should be fun, but never at the cost of accessibility.Wrapping My Head Around random()Tinkering with random() over the last couple of days was a lot of fun. I’m still not sure if I’ll be able to convince my designer friends, but what I do know is that it brings a bit of joy.Whenever we wanted a bit of organic randomness on the web, we had to make compromises: either pull in a JavaScript animation library, or write huge preprocessor loops that spit out hundreds of lines of static CSS. Having this built into the browser engine with zero JavaScript overhead is such a welcome addition.We spent the last decade mastering consistency, tokens, and design systems. Now that we have that foundation in place, maybe it’s time to let a little bit of playful messiness back onto the web.I really believe that the web is supposed to be fun.Never mind the defaults; here is CSS random().See the Pen Never mind the defaults, here is css random() [forked] by @utilitybend.