DEV Community

Yes or No Random Generator
Yes or No Random Generator

Posted on

How to Build a Fair Yes/No Spinning Wheel with JavaScript

Can't decide? Let a wheel do it. In this post I'll show you how to build a Yes/No spinning wheel with plain JavaScript, no libraries, and why Math.random() isn't the best choice for it.

I built a bigger version of this for my site, a free Yes or No Random Generator, and this is a simplified version you can copy and learn from.

What we're building

  • A wheel with 8 equal slices (4 Yes, 4 No)
  • A spin animation made with CSS
  • A fair 50/50 result using the Web Crypto API

Why not Math.random()?

Math.random() works for casual use, but browsers also give us crypto.getRandomValues(), which is designed to be unpredictable. For a decision tool it costs nothing extra, so I use it.

One catch: if you turn a random number into a range with %, some results can come up slightly more often. This helper avoids that:

function secureRandomInt(max) {
  const limit = Math.floor(0x100000000 / max) * max;
  const buf = new Uint32Array(1);
  do {
    crypto.getRandomValues(buf);
  } while (buf[0] >= limit);
  return buf[0] % max;
}
Enter fullscreen mode Exit fullscreen mode

It throws away values that would make some numbers more likely, so every slice has an equal chance.

The trick: pick the answer first, then animate

A common mistake is to spin the wheel by a random angle and then read what's under the pointer. It's easier and more reliable to pick the result first, then rotate the wheel so that slice lands under the pointer.

The full code

Save this as index.html and open it in your browser:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Yes or No Wheel</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; padding: 40px; background: #f4f6f8; }
    .wrap { position: relative; width: 260px; margin: 30px auto; }
    .pointer { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 28px; z-index: 2; }
    #wheel {
      width: 260px; height: 260px; border-radius: 50%;
      border: 6px solid #1f2937;
      transition: transform 4s cubic-bezier(.17, .67, .12, .99);
    }
    #result { font-size: 48px; font-weight: bold; min-height: 60px; margin: 20px 0; }
    button { padding: 12px 28px; font-size: 18px; background: #2563eb; color: #fff; border: 0; border-radius: 8px; cursor: pointer; }
    button:disabled { opacity: .5; cursor: not-allowed; }
  </style>
</head>
<body>
  <h1>Yes or No Wheel</h1>
  <div class="wrap">
    <div class="pointer">&#9660;</div>
    <div id="wheel"></div>
  </div>
  <div id="result"></div>
  <button id="spin">SPIN</button>

  <script>
    const wheel = document.getElementById('wheel');
    const result = document.getElementById('result');
    const button = document.getElementById('spin');
    const SLICES = 8;
    const SIZE = 360 / SLICES;

    // Build the wheel colors: even slices = Yes (green), odd = No (red)
    const stops = [];
    for (let i = 0; i < SLICES; i++) {
      const color = i % 2 === 0 ? '#16a34a' : '#dc2626';
      stops.push(`${color} ${i * SIZE}deg ${(i + 1) * SIZE}deg`);
    }
    wheel.style.background = `conic-gradient(${stops.join(',')})`;

    function secureRandomInt(max) {
      const limit = Math.floor(0x100000000 / max) * max;
      const buf = new Uint32Array(1);
      do { crypto.getRandomValues(buf); } while (buf[0] >= limit);
      return buf[0] % max;
    }

    let rotation = 0;

    button.addEventListener('click', () => {
      button.disabled = true;
      result.textContent = '';

      // 1. Pick the winning slice first
      const index = secureRandomInt(SLICES);
      const answer = index % 2 === 0 ? 'YES' : 'NO';

      // 2. Work out the angle that puts that slice under the pointer
      const sliceCenter = index * SIZE + SIZE / 2;
      const target = (360 - sliceCenter + 360) % 360;
      const delta = (target - (rotation % 360) + 360) % 360;

      // 3. Spin 5 full turns plus the extra angle
      rotation += 360 * 5 + delta;
      wheel.style.transform = `rotate(${rotation}deg)`;

      wheel.addEventListener('transitionend', () => {
        result.textContent = answer;
        result.style.color = answer === 'YES' ? '#16a34a' : '#dc2626';
        button.disabled = false;
      }, { once: true });
    });
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

How it works

  1. conic-gradient paints 8 equal slices, alternating green and red.
  2. secureRandomInt(8) picks the winning slice, so Yes and No each have exactly 4 slices out of 8, a true 50/50.
  3. We calculate the rotation that places the middle of that slice under the pointer at the top.
  4. CSS transition handles the smooth spin, and transitionend shows the answer when it stops.

Fair, not rigged

Because the result is chosen before the animation and the slices are equal, the wheel is fair. A streak of five Yes answers doesn't make a No more likely. Each spin is independent.

Try it

If you build something with it, such as a classroom picker or a Discord command, I'd love to see it in the comments.

Top comments (0)