Randomness Without Chaos

Design systems are built on consistency, but the native CSS random() function from the CSS Values and Units Level 5 spec offers a way to add controlled imperfection. Safari shipped it first, Chrome 156 is rolling it out now, and Mozilla has signalled support on the standards track.

The contrast with daily design-engineering work is the point. Spacing scales, typography locks, colour tokens and grid alignment exist so nothing drifts; a card that jumps twenty pixels on refresh is a logged bug, not expression. Reaching for randomness means deciding where it earns its place.

random( <random-caching-key>?, <min>, <max> [, by <step>]? )

In practice, current browser implementations accept 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);

Syntax Essentials

Units must match

Minimum and maximum bounds have to share the same unit type, because the parser needs to know which dimension it is resolving. Lengths, angles, time, percentages and plain unitless numbers all work:

/* 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 */

Combining unit types requires calc() inside the arguments so each side resolves correctly.

Stepping and rounding

The optional third argument defines a step size. Without it, the browser samples a continuous range; with it, the result snaps to discrete increments:

/* Only produces 0px, 15px, 30px, 45px, or 60px */
top: random(0px, 60px, 15px);

That keeps motion jumpy while landing values on a predefined grid.

Caching keys

Each evaluation of random() on a property normally yields an independent value. A square whose inline-size and block-size are both randomised would distort — unless both reference a shared caching key:

.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 point at --square-size, the engine shares one computed value. The demo contrasts an uncached row of mismatched rectangles against a cached row that holds a 1:1 ratio across refreshes:

See the Pen [Uncached vs cached random() [forked]](https://codepen.io/editor/smashingmag/pen/01a15422-91ea-716e-96b2-abf22b928537) by @utilitybend.

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);
}

Support and a Look Ahead

WebKit/Safari led, Chrome and Edge are rolling out with Chrome 156, and Firefox has positive signals. During rollout, experiments belong inside a feature query:

/* Baseline for all browsers */
.badge {
  rotate: 0deg;
}

/* Enhanced for browsers with random() */
@supports (rotate: random(-2deg, 2deg)) {
  .badge {
    rotate: random(-2.5deg, 2.5deg);
  }
}

A second function in the spec, random-item(), picks from a list of predefined tokens or keywords rather than a numeric range, which suits randomising from design tokens:

/* 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));

It has no browser support today.

Per-Letter Imperfection

Vintage book covers provide the reference: letters that are barely misaligned rather than dramatically scattered. Splitting a word into spans and giving each one a small tilt reproduces it.

See the Pen [Imperfection random() [forked]](https://codepen.io/editor/smashingmag/pen/01a1542e-7015-76ac-8e84-f406fd33f3ce) by @utilitybend.

See the Pen Imperfection random() [forked] by @utilitybend.

@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;
  }
}

A hover effect snaps every character back to straight while the resting state stays playful.

The screen reader problem

Per-character spans are read out as individual letters or with awkward pauses between them. Hide the visual spans from assistive technology and supply the full word:

<h1 class="word" aria-label="Imperfection">
  <span class="word-inner" aria-hidden="true">
    <span class="char">i</span>
    <span class="char">m</span>
    <span class="char">p</span>
    <span class="char">e</span>
    <span class="char">r</span>
    <span class="char">f</span>
    <span class="char">e</span>
    <span class="char">c</span>
    <span class="char">t</span>
    <span class="char">i</span>
    <span class="char">o</span>
    <span class="char">n</span>
  </span>
</h1>

Screen reader users get clean text; sighted users get the quirks.

Procedural Bounce

Animating the tilted letters with identical timing produces the familiar clean sine wave. Giving each letter its own jump height, duration and landing squash turns the word into characters bouncing on a trampoline.

See the Pen [Bouncy imperfection with CSS random() [forked]](https://codepen.io/editor/smashingmag/pen/01a15431-ec8e-77eb-abb5-a458f3a18855) by @utilitybend.

See the Pen Bouncy imperfection with CSS random() [forked] by @utilitybend.

@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;
    }
  }
}

The negative animation delay, --delay: random(-3s, 0s), matters. A positive delay leaves letters frozen on load until their timers expire; a negative one makes the browser behave as if the animation has already been running, so every letter is mid-rhythm immediately.

Splayed Cards: Structure Plus Wobble

Randomness alone drove the tilting-card example. A deck that fans out on scroll needs both: maths for the structure, randomness for the human imperfection.

See the Pen [Scroll drivin animations with random() tilted cards [forked]](https://codepen.io/editor/smashingmag/pen/01a15456-940a-73fd-ae4e-d3286daed556) by @utilitybend.

See the Pen Scroll drivin animations with random() tilted cards [forked] by @utilitybend.

@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);
    }
  }
}

  • sibling-index() distributes the fan evenly around the centre card and scales --elevation so higher cards cast wider shadows.
  • calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg); keeps the rotation at 12deg per card regardless of how many cards exist.
  • random() supplies the wobble of a hand-dealt deck.
  • animation-timeline: scroll(root) binds the fan and shadow transitions to the scrollbar.

Deciding Where Randomness Belongs

The uncontrolled experiments are one thing; production usage is another. Randomization works best when it decorates rather than directs.

Appropriate Targets

  • Decorative micro-imperfection: slight tilts on polaroids and sticky notes, small angles in typographic hero titles.
  • Desynchronized ambient loops: status beacons, floating badges and particle grids where synchronized motion reads as mechanical.
  • Generative backgrounds: star fields, confetti, bokeh circles, texture grain.
  • Editorial art direction: magazine spreads, event flyers, brutalist layouts, playful landing pages.
  • Random offsets layered on math: a structural grid from sibling-index() with small random deviations added for warmth.

What Must Stay Predictable

  • Core layouts and grids. Column gutters, form field widths and container padding should not be randomized; navigation depends on stable geometry.
  • Body copy. Leave font size, line height and letter spacing of long-form paragraphs alone — reading fatigue is a real cost.
  • Interactive targets. A tilted or shifted small button or link must reset on hover and focus. Users should never have to chase a crooked or moving control.

That said, the boundary is worth testing rather than treating as fixed.

Reduced Motion Comes First

Animations must honor the user's motion preference:

@media (prefers-reduced-motion: reduce) {
  .char {
    animation: none;
    rotate: 0deg;
    translate: 0 0;
  }
}

Flattening the random angles themselves, not just suppressing animation, is also worth considering — many random rotations can be disorienting while scrolling. Accessibility is not negotiable against playfulness.

The Practical Difference

A little organic randomness used to force a compromise: either add a JavaScript animation library or generate hundreds of lines of static CSS through preprocessor loops. Having random() inside the browser engine with no JavaScript overhead removes both costs.

After the past decade spent on consistency, tokens and design systems, the foundation exists to reintroduce a degree of playful messiness. The web is supposed to be fun — and now CSS has random() natively.

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.

See the Pen [Never mind the defaults, here is css random() [forked]](https://codepen.io/editor/smashingmag/pen/01a1545a-ab6a-707b-935e-9cf6dd51a3a9) by @utilitybend.

See the Pen Never mind the defaults, here is css random() [forked] by @utilitybend.
Smashing Editorial