Spin the wheel with your own prizes on it

A wheel turns "who gets what" into something everyone watches. Here is how to get a spin the wheel page with your prizes or choices, and keep it up to date.

The shop opens at ten. You have a box of tote bags, a stack of coupons and a jar of stickers.

You could hand them out. Or people could spin for them, and the whole queue would stop to watch.

These days people ask ChatGPT, Claude or Gemini for it and get a finished page in one reply. A spin the wheel page with your prizes on the slices, a pointer that clicks past each one, and confetti when it lands.

Here is one. Tap Spin.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spin to win</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 50% 18%, #6b2bd9 0%, #3a1580 38%, #1c0a45 75%); color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; overflow-x: hidden; }
  .page { max-width: 560px; margin: 0 auto; padding: 18px 14px 28px; text-align: center; }
  .shop { font-size: 13px; letter-spacing: .14em; text-transform: uppercase; color: #ffcf5c; }
  h1 { margin: 4px 0 34px; font-size: 28px; letter-spacing: -.01em; }
  .wrap { position: relative; width: min(340px, 86vw); margin: 0 auto; aspect-ratio: 1; }
  .wrap::before { content: ""; position: absolute; inset: -12px; border-radius: 50%; background: conic-gradient(from 0deg, #ffcf5c, #ff8a3d, #ffcf5c, #ff8a3d, #ffcf5c); box-shadow: 0 18px 40px rgba(0,0,0,.45); }
  .wrap::after { content: ""; position: absolute; inset: -12px; border-radius: 50%; background: repeating-conic-gradient(from 0deg, transparent 0 13deg, rgba(255,255,255,.95) 13deg 15deg); -webkit-mask: radial-gradient(circle, transparent calc(50% - 7px), #000 calc(50% - 6px), #000 calc(50% - 2px), transparent calc(50% - 1px)); mask: radial-gradient(circle, transparent calc(50% - 7px), #000 calc(50% - 6px), #000 calc(50% - 2px), transparent calc(50% - 1px)); opacity: .8; }
  #wheel { position: relative; z-index: 1; width: 100%; height: 100%; display: block; border-radius: 50%; }
  .hub { position: absolute; z-index: 2; left: 50%; top: 50%; width: 22%; aspect-ratio: 1; transform: translate(-50%, -50%); border-radius: 50%; border: 0; background: radial-gradient(circle at 35% 30%, #fff, #ffe3a1 45%, #f0a020); box-shadow: 0 4px 12px rgba(0,0,0,.4); font: 900 clamp(13px, 3.6vw, 16px) system-ui; color: #4a1d00; cursor: pointer; }
  .hub:disabled { cursor: default; }
  .pointer { position: absolute; z-index: 3; left: 50%; top: -22px; width: 34px; height: 46px; margin-left: -17px; transform-origin: 50% 12px; filter: drop-shadow(0 3px 3px rgba(0,0,0,.4)); }
  .spin { margin-top: 24px; border: 0; border-radius: 18px; padding: 15px 44px; background: linear-gradient(180deg, #ffd56b, #ff9b2f); color: #4a1d00; font: 900 20px system-ui; letter-spacing: .04em; cursor: pointer; box-shadow: 0 5px 0 #b8600f; transition: transform .1s, box-shadow .1s; }
  .spin:active { transform: translateY(4px); box-shadow: 0 1px 0 #b8600f; }
  .spin:disabled { opacity: .6; cursor: default; }
  .banner { margin: 16px auto 0; max-width: 380px; padding: 14px 16px; border-radius: 18px; background: #fff; color: #2a0f5c; box-shadow: 0 12px 30px rgba(0,0,0,.35); transform: scale(.4); opacity: 0; transition: transform .45s cubic-bezier(.2,1.6,.4,1), opacity .2s; }
  .banner.show { transform: none; opacity: 1; }
  .banner .k { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: #8a5cd6; }
  .banner .p { font: 900 26px/1.2 system-ui; margin: 2px 0 4px; }
  .banner .s { font-size: 14px; color: #5c4a80; }
  .list { margin: 22px auto 0; max-width: 420px; text-align: left; padding: 12px 16px; border-radius: 16px; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12); }
  .list h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: #ffcf5c; }
  .list ol { margin: 0; padding-left: 22px; columns: 2; column-gap: 22px; font-size: 15px; line-height: 1.75; }
  .list p { margin: 6px 0 0; font-size: 13px; color: #c9b8ef; }
  #fx { position: fixed; inset: 0; pointer-events: none; z-index: 9; }
</style>
</head>
<body>
<main class="page">
  <div class="shop">Maple and Thread · Opening day</div>
  <h1>Spin to win</h1>
  <div class="wrap">
    <svg class="pointer" id="pointer" viewBox="0 0 34 46" aria-hidden="true"><path d="M17 46 L3 14 A14 14 0 1 1 31 14 Z" fill="#fff"/><circle cx="17" cy="13" r="6" fill="#e0457b"/></svg>
    <canvas id="wheel" width="680" height="680" aria-label="Prize wheel"></canvas>
    <button class="hub" id="hub" aria-label="Spin">SPIN</button>
  </div>
  <button class="spin" id="spin">Spin the wheel</button>
  <div class="banner" id="banner"><div class="k">You won</div><div class="p" id="prize">...</div><div class="s">Show this screen at the counter.</div></div>

  <section class="list">
    <h2>Prizes on the wheel</h2>
    <ol id="prizes">
      <li>10% off</li>
      <li>Free tote bag</li>
      <li>Sticker pack</li>
      <li>Free coffee</li>
      <li>15% off</li>
      <li>Pin badge</li>
      <li>Free gift wrap</li>
      <li>Spin again</li>
    </ol>
    <p>Shop and prizes are made up. Every slice is the same size and has the same chance.</p>
  </section>
</main>
<canvas id="fx"></canvas>
<script>
  const cv = document.getElementById('wheel'), x = cv.getContext('2d');
  const pointer = document.getElementById('pointer');
  const COLORS = ['#ff5c8a', '#ffb02e', '#2ec4b6', '#5b7cfa', '#ff7a45', '#a35cff', '#22b573', '#f2558f', '#3fa7ff', '#ffd23f'];
  let angle = 0, spinning = false;

  // Slices come from the list on the page, so the wheel always matches it.
  function slices() { return [...document.querySelectorAll('#prizes li')].map((li) => li.textContent.trim()).filter(Boolean); }

  function draw() {
    const items = slices(), n = items.length || 1, W = cv.width, r = W / 2;
    x.clearRect(0, 0, W, W);
    x.save(); x.translate(r, r); x.rotate(angle);
    const seg = Math.PI * 2 / n;
    for (let i = 0; i < n; i++) {
      // Slice i is centred at the top (-90deg) when angle = -i * seg.
      const a0 = -Math.PI / 2 - seg / 2 + i * seg;
      x.beginPath(); x.moveTo(0, 0); x.arc(0, 0, r, a0, a0 + seg); x.closePath();
      x.fillStyle = COLORS[i % COLORS.length]; x.fill();
      const g = x.createRadialGradient(0, 0, r * .2, 0, 0, r);
      g.addColorStop(0, 'rgba(255,255,255,.18)'); g.addColorStop(1, 'rgba(0,0,0,.12)');
      x.fillStyle = g; x.fill();
      x.strokeStyle = 'rgba(255,255,255,.85)'; x.lineWidth = 4; x.stroke();
      x.save(); x.rotate(a0 + seg / 2);
      x.fillStyle = '#fff'; x.textAlign = 'right'; x.textBaseline = 'middle';
      const label = items[i] || '';
      let size = n > 10 ? 26 : 34;
      x.font = '800 ' + size + 'px system-ui, sans-serif';
      while (x.measureText(label).width > r * .6 && size > 16) { size -= 2; x.font = '800 ' + size + 'px system-ui, sans-serif'; }
      x.shadowColor = 'rgba(0,0,0,.35)'; x.shadowBlur = 6;
      x.fillText(label, r - 26, 0);
      x.restore();
    }
    x.restore();
  }

  function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }

  function spin() {
    if (spinning) return;
    const items = slices(); if (!items.length) return;
    spinning = true;
    document.getElementById('spin').disabled = true; document.getElementById('hub').disabled = true;
    document.getElementById('banner').classList.remove('show');
    const n = items.length, seg = Math.PI * 2 / n;
    // Choose the slice first with Math.random (each slice the same chance), then work out where to stop.
    const win = Math.floor(Math.random() * n);
    const wobble = (Math.random() - .5) * seg * .7;
    const base = -win * seg + wobble;
    const turns = 5 + Math.floor(Math.random() * 3);
    let target = base;
    while (target < angle + Math.PI * 2 * turns) target += Math.PI * 2;
    const start = angle, dist = target - start, dur = 4800 + Math.random() * 800, t0 = performance.now();
    let lastSlice = null, kick = 0;
    (function frame(now) {
      const t = Math.min(1, (now - t0) / dur);
      angle = start + dist * easeOutQuart(t);
      draw();
      // Pointer flicks each time a slice edge passes it.
      const s = Math.floor((-angle + seg / 2) / seg);
      if (lastSlice !== null && s !== lastSlice) kick = 1;
      lastSlice = s;
      kick *= .82;
      pointer.style.transform = 'rotate(' + (kick * 24) + 'deg)';
      if (t < 1) return requestAnimationFrame(frame);
      angle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
      pointer.style.transform = '';
      done(items[win]);
    })(t0);
  }

  function done(prize) {
    document.getElementById('prize').textContent = prize;
    document.getElementById('banner').classList.add('show');
    confetti();
    spinning = false;
    document.getElementById('spin').disabled = false; document.getElementById('hub').disabled = false;
  }

  function confetti() {
    const c = document.getElementById('fx'), g = c.getContext('2d');
    c.width = innerWidth; c.height = innerHeight;
    const bits = Array.from({ length: 160 }, () => ({ x: Math.random() * c.width, y: -20 - Math.random() * c.height * .5, vx: (Math.random() - .5) * 3, vy: 2 + Math.random() * 3.5, a: Math.random() * 6, w: 5 + Math.random() * 6, col: COLORS[Math.floor(Math.random() * COLORS.length)] }));
    let f = 0;
    (function anim() {
      g.clearRect(0, 0, c.width, c.height);
      bits.forEach((b) => { b.x += b.vx + Math.sin(f / 10 + b.a); b.y += b.vy; b.a += .12; g.save(); g.translate(b.x, b.y); g.rotate(b.a); g.fillStyle = b.col; g.fillRect(-b.w / 2, -2, b.w, 5); g.restore(); });
      if (++f < 170) requestAnimationFrame(anim); else g.clearRect(0, 0, c.width, c.height);
    })();
  }

  document.getElementById('spin').onclick = spin;
  document.getElementById('hub').onclick = spin;
  // Redraw if someone edits the prize list on the page.
  new MutationObserver(() => { if (!spinning) draw(); }).observe(document.getElementById('prizes'), { childList: true, characterData: true, subtree: true });
  draw();
</script>
</body>
</html>
A prize wheel for a shop opening. The slices are drawn from the prize list under the wheel, the spin eases out, the pointer flicks at each slice edge, and a banner shows the prize. The shop and prizes are made up. Edit the code and the example reruns.

Three things that make a wheel people trust

1. The slice people see is the chance they get. If every prize has the same chance, make every slice the same size. If the grand prize should be rare, ask for a thinner slice. A big slice that hardly ever wins feels like a trick once someone notices.

2. The pointer and the banner must agree. Ask the AI to choose the result first, then spin the wheel so it stops on that slice. If the page spins first and works out the winner afterwards, the banner and the pointer can disagree on a slice edge.

3. Short labels. A slice is narrow near the middle. "Free tote bag" fits. "One free canvas tote bag while stocks last" shrinks to a size nobody can read from a step away.

Two wheels. Six equal slices, each 1 in 6. A rare grand prize on a thin slice, 1 in 12, next to wider common slices.
Two wheels. Six equal slices, each 1 in 6. A rare grand prize on a thin slice, 1 in 12, next to wider common slices.

Make it with AI

Open ChatGPT, Claude or Gemini and paste this. Change the words in capitals and add your list at the end.

Make one complete HTML file: a spin the wheel page.
Title: TITLE. Colours: COLOURS.
Show my prizes as a plain numbered list under the
wheel, and draw the wheel slices from that list.
Every slice the same size and the same chance.
Choose the winning slice first with Math.random,
then spin about five turns and ease out so the
pointer stops on that slice.
A pointer at the top that flicks as slices pass.
When it stops, show a banner with the prize and a
short confetti burst.
Rules: all CSS and JavaScript inside the file, no
images or fonts from other sites, nothing sent
anywhere, works on a phone and a tablet.
Use only my prizes, do not invent any.
Prizes: PASTE YOUR LIST HERE

"Draw the wheel slices from that list" matters most. It keeps each prize as plain text on the page, so swapping one later does not mean touching the code that draws the wheel.

A wheel also settles everyday arguments. This one picks team lunch and keeps a running count, so you can watch how the results spread out over many spins.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Where's team lunch?</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; min-height: 100vh; background: linear-gradient(180deg, #fff6ea, #fdebd8); color: #2d1f14; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .page { max-width: 560px; margin: 0 auto; padding: 18px 14px 28px; }
  h1 { margin: 0; font-size: 24px; text-align: center; }
  .sub { text-align: center; font-size: 14px; color: #8a6a52; margin: 4px 0 14px; }
  .stage { position: relative; width: min(300px, 78vw); margin: 0 auto; aspect-ratio: 1; }
  .stage svg.w { width: 100%; height: 100%; display: block; filter: drop-shadow(0 12px 18px rgba(120,60,10,.25)); }
  #rot { transform-origin: 150px 150px; transition: transform 4.2s cubic-bezier(.12,.72,.12,1); }
  .tick { position: absolute; top: 50%; right: -16px; width: 30px; height: 26px; margin-top: -13px; filter: drop-shadow(0 2px 2px rgba(0,0,0,.25)); }
  .go { display: block; margin: 18px auto 0; border: 0; border-radius: 999px; padding: 13px 34px; background: #2d1f14; color: #fff6ea; font: 800 17px system-ui; cursor: pointer; box-shadow: 0 4px 0 #000; transition: transform .1s; }
  .go:active { transform: translateY(3px); }
  .go:disabled { opacity: .5; cursor: default; }
  .result { text-align: center; min-height: 54px; margin-top: 12px; }
  .result .r { display: inline-block; padding: 9px 18px; border-radius: 14px; background: #fff; box-shadow: 0 6px 16px rgba(120,60,10,.15); font: 800 22px system-ui; animation: pop .5s cubic-bezier(.2,1.6,.4,1); }
  @keyframes pop { from { transform: scale(.5); opacity: 0; } }
  .panel { margin-top: 14px; padding: 14px; border-radius: 18px; background: #fff; box-shadow: 0 4px 14px rgba(120,60,10,.08); }
  .panel h2 { margin: 0 0 10px; font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: #8a6a52; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
  .panel h2 button { border: 0; border-radius: 9px; padding: 6px 10px; background: #f5e6d6; color: #2d1f14; font: 700 12px system-ui; cursor: pointer; letter-spacing: 0; text-transform: none; }
  .row { display: grid; grid-template-columns: 96px 1fr 34px; gap: 8px; align-items: center; font-size: 14px; margin-bottom: 6px; }
  .row .nm { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
  .bar { height: 14px; border-radius: 7px; background: #f5ece2; overflow: hidden; }
  .bar i { display: block; height: 100%; width: 0; border-radius: 7px; transition: width .4s; }
  .row .c { text-align: right; font: 700 13px ui-monospace, Consolas, monospace; color: #8a6a52; }
  .note { margin: 8px 0 0; font-size: 13px; color: #8a6a52; }
  .opts { margin: 0; padding-left: 20px; font-size: 15px; line-height: 1.7; }
</style>
</head>
<body>
<main class="page">
  <h1>Where's team lunch?</h1>
  <p class="sub">Friday decider. The wheel spins, nobody argues.</p>
  <div class="stage">
    <svg class="w" viewBox="0 0 300 300"><g id="rot"></g><circle cx="150" cy="150" r="26" fill="#2d1f14"/><circle cx="150" cy="150" r="10" fill="#fff6ea"/></svg>
    <svg class="tick" viewBox="0 0 30 26"><path d="M0 13 L30 0 L30 26 Z" fill="#2d1f14"/></svg>
  </div>
  <button class="go" id="go">Spin</button>
  <div class="result" id="result"></div>

  <section class="panel">
    <h2>Results so far <span><button id="sim">Spin 60 times fast</button> <button id="clear">Clear</button></span></h2>
    <div id="tally"></div>
    <p class="note" id="total">No spins yet. Each option has the same chance, so over many spins the bars come out roughly even, rarely exactly.</p>
  </section>

  <section class="panel">
    <h2>Options</h2>
    <ol class="opts" id="opts">
      <li>Tacos</li>
      <li>Ramen</li>
      <li>Salad bar</li>
      <li>Pizza</li>
      <li>Thai</li>
      <li>Burgers</li>
    </ol>
  </section>
</main>
<script>
  const COLORS = ['#ff7a45', '#ffc93c', '#5cc8a8', '#ff5c7a', '#7a8cff', '#b08968', '#e86fd1', '#47b8ff'];
  const rot = document.getElementById('rot');
  let turn = 0, spinning = false, counts = {};

  // The options are read from the list on the page.
  function options() { return [...document.querySelectorAll('#opts li')].map((li) => li.textContent.trim()).filter(Boolean); }

  function esc(s) { return s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }

  function build() {
    const o = options(), n = o.length || 1, seg = 360 / n;
    let h = '';
    o.forEach((name, i) => {
      // Slice i is centred at 0deg (pointing right, at the pointer) when the wheel is turned by -i * seg.
      const a0 = (i * seg - seg / 2) * Math.PI / 180, a1 = (i * seg + seg / 2) * Math.PI / 180;
      const x0 = 150 + 140 * Math.cos(a0), y0 = 150 + 140 * Math.sin(a0), x1 = 150 + 140 * Math.cos(a1), y1 = 150 + 140 * Math.sin(a1);
      h += '<path d="M150 150 L' + x0.toFixed(2) + ' ' + y0.toFixed(2) + ' A140 140 0 ' + (seg > 180 ? 1 : 0) + ' 1 ' + x1.toFixed(2) + ' ' + y1.toFixed(2) + ' Z" fill="' + COLORS[i % COLORS.length] + '" stroke="#fff6ea" stroke-width="3"/>';
      const fs = name.length > 9 ? 13 : 16;
      h += '<text x="268" y="150" transform="rotate(' + (i * seg) + ' 150 150)" text-anchor="end" dominant-baseline="middle" font-family="system-ui, sans-serif" font-weight="800" font-size="' + fs + '" fill="#fff" style="paint-order:stroke" stroke="rgba(0,0,0,.18)" stroke-width="2">' + esc(name) + '</text>';
    });
    h += '<circle cx="150" cy="150" r="140" fill="none" stroke="#2d1f14" stroke-width="6"/>';
    rot.innerHTML = h;
  }

  function tally() {
    const o = options(), max = Math.max(1, ...o.map((k) => counts[k] || 0));
    const total = o.reduce((a, k) => a + (counts[k] || 0), 0);
    document.getElementById('tally').innerHTML = o.map((k, i) => '<div class="row"><span class="nm">' + esc(k) + '</span><span class="bar"><i style="width:' + ((counts[k] || 0) / max * 100) + '%;background:' + COLORS[i % COLORS.length] + '"></i></span><span class="c">' + (counts[k] || 0) + '</span></div>').join('');
    if (total) document.getElementById('total').textContent = total + ' spins. Each option has a 1 in ' + o.length + ' chance per spin, so the bars wobble around an even split.';
  }

  function spin() {
    if (spinning) return;
    const o = options(); if (!o.length) return;
    spinning = true; document.getElementById('go').disabled = true; document.getElementById('sim').disabled = true;
    document.getElementById('result').innerHTML = '';
    build();
    const n = o.length, seg = 360 / n;
    const win = Math.floor(Math.random() * n);
    const wobble = (Math.random() - .5) * seg * .6;
    // Next resting angle: at least 5 full turns past the current one, stopping with slice "win" at the pointer.
    let target = -win * seg + wobble;
    while (target < turn + 360 * 5) target += 360;
    turn = target;
    rot.style.transform = 'rotate(' + turn + 'deg)';
    setTimeout(() => {
      counts[o[win]] = (counts[o[win]] || 0) + 1;
      document.getElementById('result').innerHTML = '<span class="r">' + esc(o[win]) + '</span>';
      tally();
      spinning = false; document.getElementById('go').disabled = false; document.getElementById('sim').disabled = false;
    }, 4300);
  }

  document.getElementById('go').onclick = spin;
  document.getElementById('sim').onclick = () => {
    if (spinning) return;
    const o = options();
    for (let k = 0; k < 60; k++) { const w = o[Math.floor(Math.random() * o.length)]; counts[w] = (counts[w] || 0) + 1; }
    tally();
  };
  document.getElementById('clear').onclick = () => {
    counts = {}; tally();
    document.getElementById('total').textContent = 'No spins yet. Each option has the same chance, so over many spins the bars come out roughly even, rarely exactly.';
  };
  new MutationObserver(() => { if (!spinning) { build(); tally(); } }).observe(document.getElementById('opts'), { childList: true, characterData: true, subtree: true });
  build(); tally();
</script>
</body>
</html>
A decision wheel for team lunch. Options come from the list on the page, each spin is counted, and "Spin 60 times fast" shows how results spread out. Edit the code and the example reruns.

Getting the wheel to the booth

You have a spin the wheel page. The AI gave it to you as HTML code, though, and code does not spin for anyone.

The first wall is putting it where people can tap it. Send the file to the shop tablet or to customers, and on a phone it often opens as code or lands in downloads. To be a page people tap, it has to live somewhere.

In NOS you paste the code into a document and it becomes the wheel straight away. Choose Create share link. Open the link on the booth tablet, or print it as a QR code so people spin on their own phones. Nobody needs an account.

Four steps: copy the AI reply, paste it in NOS, create a share link, open it on the booth tablet or print it as a QR code.
Four steps: copy the AI reply, paste it in NOS, create a share link, open it on the booth tablet or print it as a QR code.

The second wall arrives at two in the afternoon. The tote bags run out. You have mugs instead, and the wheel still promises a tote bag.

If you ask the AI again, you wait for a new reply. It often changes the colours or the spin while it is at it, so you test the wheel again. Then you have a new file and a new link, and the QR sign on the table points to the old one.

In NOS you click "Free tote bag" in the prize list on the page and type "Free mug". The wheel redraws from the list. The link stays the same, so the next customer who scans the sign spins for a mug.

"Free tote bag" changed to "Free mug" in the prize list in NOS. The same link now shows a wheel with a mug slice.
"Free tote bag" changed to "Free mug" in the prize list in NOS. The same link now shows a wheel with a mug slice.

Before you share it

The page picks with Math.random, the browser's built-in random number function. MDN notes that it is not cryptographically secure. That is fine for coupons and lunch, not for a lottery with money at stake.

The wheel cannot tell who already spun. Each visitor's browser runs its own copy, so a reload means another spin. If one spin per person matters, have someone at the booth watch.

To pick a person rather than a prize, see Random name picker. For the page around your event, see Event landing page.

Questions people ask

How does a spin the wheel page decide where it stops?

A good one picks the slice first with Math.random, then spins and eases the wheel so the pointer lands on that slice. That way the banner always matches what people see under the pointer.

Can one prize be rarer than the others?

Yes. Ask the AI to give each prize a weight and to draw each slice as wide as its chance. A thin slice for the grand prize is honest, because people can see it is rare.

Can it stop someone from spinning twice?

No. The page runs in each visitor’s browser and does not know who spun before. At a booth, have someone watch the spin and hand over the prize.

Can I change a prize during the event?

Yes. In NOS you click the prize in the list on the page and type. The wheel redraws from the list, and the same link shows the new prize.

Do visitors need an account?

No. A NOS share link opens in an ordinary browser on a phone or tablet. Visitors can spin but cannot edit the page.

Keep reading