Skip to main content

Command Palette

Search for a command to run...

Fair Wheels Use crypto.getRandomValues(), Not Math.random()

A look at unbiased selection on a spinning decision wheel — rejection sampling, seedless fairness, and a free tool you can try today.

Updated
•3 min read•View as Markdown
Fair Wheels Use crypto.getRandomValues(), Not Math.random()
B
Founder of ToolkitLoop (toolkitloop.com) — 30 free online tools that run 100% in your browser. No sign-ups, no uploads, private by design.

Every "spin the wheel" widget you've seen has two halves, and only one of them matters. The animation — easing curves, tick sounds, confetti — is theater. The selection is math. If the math is wrong, the theater is a lie wearing confetti.

Most wheel implementations pick like this:

segments[Math.floor(Math.random() * segments.length)]

For a yes/no decision wheel this is fine. Math.random() is deterministic and theoretically predictable, but nobody is attacking your lunch decision. The problem starts when the wheel picks giveaway winners, assigns on-call rotations, or does anything where someone can lose something. Then "almost uniform" stops being good enough, and modulo bias — the slight unevenness of Math.floor(Math.random() * n) for certain n — becomes a real, if small, unfairness.

Rejection sampling in 15 lines

The Web Crypto API exposes OS-level entropy via crypto.getRandomValues(). Turning random 32-bit integers into a uniform index in [0, n) without modulo bias is a textbook rejection sample:

function fairIndex(n) {
  const max = 0xffffffff;
  const limit = max - (max % n); // reject the biased tail
  const buf = new Uint32Array(1);
  let x;
  do {
    crypto.getRandomValues(buf);
    x = buf[0];
  } while (x >= limit);
  return x % n;
}

Values at or above limit would map unevenly onto the segments, so we throw them away and draw again. The expected retry count is ~1.0 — the loop is practically free, and the distribution is exactly uniform.

Architecture: selection before animation

Clean wheel code separates the two halves completely:

  1. Select the winning index with fairIndex() — pure function, unit-testable.
  2. Derive the final rotation angle from the index (a few full turns for drama, then land mid-slice).
  3. Animate to that angle with CSS transitions.

Because selection is decoupled, you can verify fairness offline: run 100,000 draws, histogram the results, and confirm uniformity before any pixel moves. If you're building any picker — wheels, lotteries, shuffles — this test is cheap and catches real bugs (off-by-one slices, rounding in angle math, you name it).

Seedless, serverless, stateless

The whole pipeline runs client-side. There's no seed to protect, no server to trust, and — relevant if your wheel handles entrant names — nothing uploaded anywhere. For a small giveaway tool, "we never see your list" is the simplest privacy policy there is.

Disclosure

I built a free wheel implementing exactly this at toolkitloop.com/yes-no-wheel — yes/no mode plus custom segments, no signup. It's one of 30 client-side tools I maintain at ToolkitLoop. The patterns above are general; adapt them freely.

If you've ever audited randomness in a picker, shuffler, or A/B splitter — what did you find? The subtle bugs are always more interesting than the obvious ones.