Bringing CSS random() to the Rest of Us

The CSS random() function has been live in Safari since late 2025, but Chrome and Firefox remain quiet on release dates. For developers outside the Apple ecosystem, this means watching demos from the WebKit team and waiting. The feature is part of an editor’s draft spec that is still in “early exploration phase,” so polyfilling it is no small task—the syntax is intricate, with caching semantics, base values, and intervals to handle.

Yet that is exactly what I set out to do with the css-random-polyfill package. The approach avoids the usual CSS polyfill minefield because, unlike a new selector, a new function can be written in technically valid CSS that even browsers without native support will parse. The trick: store any random value in a custom property that starts with the --random prefix, and the polyfill script takes care of the rest.

The Starfield Demo, Cross-Browser

The first test is a port of Apple’s starfield demo, which scatters stars randomly and fades them at random intervals. Large four-pointed stars tilt at a shared angle, and each star gets a subtle, random hue on its shadow. Adapting the original Safari-only version for Chrome and Firefox requires two HTML changes: load the polyfill script and add a randomized marker class to each element being targeted.

<!-- the script processes usages of css random on page load -->
<script src="https://unpkg.com/css-random-polyfill@latest/dist/css-random-polyfill.js"></script>

<!-- 200 star divs, we add the "randomized" marker class so css-random-polyfill knows which elements to target  -->
<div class="randomized star"></div>
<div class="randomized star"></div>
<!-- etc. -->
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>
<div class="randomized star fourpointed"></div>

The CSS itself remains valid in all browsers. Every random value passes through an intermediate custom property, and the polyfill substitutes real numbers at runtime. This example shows two syntax variations from the spec: the optional third argument for a step interval, which here forces whole-number values within the range, and the element-shared base value, which makes all four-pointed stars share one randomly chosen tilt angle.

.star {
  --random-star-size: random(1px, 7px, 1px);
  background-color: white;
  border-radius: 50%;
  aspect-ratio: 1/1;
  width: var(--random-star-size);
  position: fixed;

  --random-top: random(0%, 100%);
  --random-left: random(0%, 100%);
  top: var(--random-top);
  left: var(--random-left);

  --random-hue: random(0, 360);
  filter: drop-shadow(0px 0px calc(var(--random-star-size) * 0.7) oklch(0.7 0.2 var(--random-hue)))
    drop-shadow(0px 0px calc(var(--random-star-size) * 3) white);
  mix-blend-mode: hard-light;

  --random-speed: random(2s, 5s);
  animation: fade-in var(--random-speed);
  animation-iteration-count: infinite;

  --random-delay: random(2s, 5s);
  animation-delay: var(--random-delay);
  animation-direction: normal;
}
--random-star-size: random(1px, 7px, 1px);
.star.fourpointed {
  --random-rotation: random(element-shared, -45deg, 45deg);
  rotate: var(--random-rotation);
}

Native random() can be used inline in any property value, much like calc() or min(). The polyfill necessarily requires a bit more ceremony, but the trade-off is worth it: the CSS stays compatible with the native implementation. If you delete the polyfill script once random() goes baseline, your code still works. In Safari today, the polyfill detects native support and does nothing, letting the browser handle the randomness natively.

Randomized Grid Cells and Shorthand Properties

A second demo from the Safari team, a 100×100 grid with randomly colored cells, is more contrived than compelling—but it exercises a few more corners of the syntax. The polyfill handles flexible input, including custom properties passed into random() and multiple random() calls in a single value. That means shorthand properties like grid-area can carry randomized row-start and column-start values all at once.

.rectangle {
  --random-grid-area: random(1, var(--rows), 1) / random(1, var(--columns), 1);
  grid-area: var(--random-grid-area);
}

That flexibility, combined with the fact that the code you write today remains valid for tomorrow’s native support, makes the added ceremony a reasonable price. We get to experiment with controlled randomness now, without waiting for the other browsers to roll the dice.

Wheel of fortune and random squares

The wheel of fortune demo, originally from Tim Nguyen on the Safari team, shows that the step interval parameter of random() can use a different unit than the minimum and maximum parameters. The specification requires values to be “resolvable to the same data type,” so mixing turn and deg works the same way CSS calc() combines units via typed arithmetic.

@keyframes spin {
  from {
    rotate: 0deg;
  }
  to {
    rotate: var(--random-rotation);
  }
}

#wheel {
  --random-rotation: random(2turn, 10turn, 20deg);
}

A note on the polyfill: the variable had to be defined in a CSS class applied when the polyfill first loads, rather than inside a keyframes animation triggered by a checkbox hack. The polyfill processes only computed styles applied to elements on initial page load. Dynamic responses to computed style changes or DOM mutations remain future work.

The random squares demo, from Chris Coyier, is about the simplest possible use of CSS random(), with three squares each getting a random position and color. The cross-browser version below also randomizes the size of each square, which verifies that the polyfill supports random value sharing via custom keys.

--random-height: random(--side, 40px, 100px);
--random-width: random(--side, 40px, 100px);

width: var(--random-height);
height: var(--random-width);

Using a custom key to set height and width to the same random value confirms the syntax works. In a native implementation, this would let the same random number generate both properties without an intermediate variable. The polyfill version needs such variables, so sharing a single custom property like --side would also suffice.

Simulating random-item() in Chromium

The demos above get random colors by feeding random numeric values into color functions such as rgb() or lch(). For picking from a specific list of colors, the specification defines random-item(), which takes a random-caching-options argument followed by a variable-length list of values to choose from. No browser ships it yet, aside from experimental Safari Technology Preview support.

random-item(element-shared, red, blue, green);

The polyfill does not attempt to implement random-item(). But Chromium now supports CSS custom functions and inline conditionals, and combining those with CSS random() gets very close to the missing feature.

--random-index: random(element-shared, 1, 5, 1);
--random-color: --item(var(--random-index), aqua, purple, pink, grey, green);

This generic --item function takes an --index argument followed by up to 10 optional arguments, which defaults to an empty value when not provided. CSS custom functions cannot accept variable-length argument lists like JavaScript functions, so the --index maps to the matching argument position.

@function --item(--index,
  --arg-1: ,
  --arg-2: ,
  --arg-3: ,
  --arg-4: ,
  --arg-5: ,
  --arg-6: ,
  --arg-7: ,
  --arg-8: ,
  --arg-9: ,
  --arg-10: ) {

  result: if(
    style(--index: 1): var(--arg-1);
    style(--index: 2): var(--arg-2);
    style(--index: 3): var(--arg-3);
    style(--index: 4): var(--arg-4);
    style(--index: 5): var(--arg-5);
    style(--index: 6): var(--arg-6);
    style(--index: 7): var(--arg-7);
    style(--index: 8): var(--arg-8);
    style(--index: 9): var(--arg-9);
    else: var(--arg-10);
  );
}

This approach is more general than earlier work on selecting from a list of colors via an --index variable, which was hardcoded to the color data type and described by its author as a hack. A custom function handles lists of any data type using CSS standards as intended.

How the polyfill works

The polyfill is not original code. An open-source PostCSS plugin for random() already existed, wrapping the MIT-licensed @csstools/css-calc. Although the package readme mentions only CSS Values and Units Module Level 4, its commit history shows an update to the latest random() specification with passing tests. The main challenge was hooking that build-time tool into client-side CSS.

import { calc } from "@csstools/css-calc";
const calcFn = calc;

if (!CSS.supports("width", "random(0px, 100px)")) {
  const styleTag = document.createElement("style");
  styleTag.textContent = ".randomized { display: none; }";
  document.head.appendChild(styleTag);
  const elementIDs = new WeakMap();
  const documentID = crypto.randomUUID();

  document.querySelectorAll(".randomized").forEach((element) => {
    const styles = getComputedStyle(element);
    [...styles]
      .filter((property) => property.startsWith("--random"))
      .forEach((propertyName) => {
        const css = styles.getPropertyValue(propertyName);
        const value = resolveRandom(css, {
          element,
          propertyName,
          documentID,
          elementIDs,
          calcFn,
          crypto,
        });
      element.style.setProperty(propertyName, value);
    });
  });
  if (styleTag.parentNode) {
    styleTag.parentNode.removeChild(styleTag);
  }
}

function resolveRandom(css, { element, propertyName, documentID, elementIDs, calcFn, crypto }) {
  const patchedCss = css.replace(
    /random\(\s*(?!(?:[^,]*\b(?:shared|scoped)\b|fixed\b|--))([^,]+),/gi,
    (_, expression) => `random(fixed ${Math.random()}, ${expression},`
  );

  return calcFn(patchedCss, {
    precision: 5,
    toCanonicalUnits: true,
    randomCaching: {
      documentID,
      elementID: elementIDs.getOrInsert(element, `element-${crypto.randomUUID()}`),
      propertyName,
    },
  });
}

The polyfill logic in plain language:

  1. If the browser natively supports CSS random(), the polyfill does nothing.
  2. Otherwise, elements marked with the .randomized class are temporarily hidden to avoid flicker.
  3. All custom properties prefixed with --random on those elements are processed.
  4. Because custom property values have permissive syntax, an expression like random(1, var(--rows), 1) is treated as a string, while the browser resolves embedded var() references in the computed value.
  5. Unique identifiers are generated for the document and each element, letting CSS Tools respect caching rules such as element-shared.
  6. If no base is provided, a fixed random one is injected; without it, the library clustered values unevenly.
  7. The resolved value is set via inline style on the element.
  8. The temporary hiding declaration is removed.

Step 4 is significant. Custom property values that contain unresolved expressions are one of the few documented extension points in CSS. Because these values are readable via JavaScript, the polyfill avoids the usual pitfalls of rewriting stylesheets or reimplementing CSS parsing.

Random closing thoughts

Good fortune has it that an open source solution already exists to bring CSS random() to browsers lacking native support. The polyfill bridges the gap nicely while we wait for broader adoption. While many developers have expressed eagerness for this feature to land universally, it remains to be seen whether they will begin using it now or hold out.

The enthusiasm in the community is palpable — reactions to the starfield demo have been particularly infectious, mirroring the excitement felt when the polyfill first enabled the effect across browsers. For those with creative ideas waiting on this capability, having a working polyfill may unlock new projects. The existence of this tool itself was born from the desire to push random() into more advanced territories.

Until next time, happy randomizing from your friendly neighbourhood random guy.