Random numbers in JavaScript, from Math.random() to a fair shuffle

One formula gives a random whole number between any two values. The rest of this guide is the small mistakes that quietly make some results rarer than others, and how to avoid them.

To get a random whole number between min and max in JavaScript, both included, use this line:

const n = Math.floor(Math.random() * (max - min + 1)) + min;

Try it below. Change min and max, roll once, or roll 10,000 times and look at the bars. Then switch the formula to one of the two common mistakes and watch which bars shrink.

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>Random integer between min and max</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; margin-bottom: 10px; }
  label { font-size: 14px; }
  input[type=number] { width: 64px; padding: 6px; font-size: 15px; }
  select { padding: 6px; font-size: 14px; max-width: 100%; }
  button { padding: 8px 14px; font-size: 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  button.alt { background: #fff; color: #1d4ed8; border: 1px solid #1d4ed8; }
  code { display: block; font: 13px ui-monospace, Consolas, monospace; background: #fff; border: 1px solid #e1e4ea;
         border-radius: 8px; padding: 8px 10px; overflow-x: auto; white-space: pre; }
  #one { font-size: 28px; font-weight: 700; min-width: 40px; }
  #chart { display: flex; align-items: flex-end; gap: 3px; height: 190px; padding: 8px 6px 0;
           background: #fff; border: 1px solid #e1e4ea; border-radius: 8px; margin-top: 10px; }
  .bar { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; min-width: 0; }
  .fill { width: 100%; background: #16a34a; border-radius: 4px 4px 0 0; }
  .fill.low { background: #ea580c; }
  .cnt { font-size: 11px; color: #5b6270; }
  .lab { font-size: 12px; font-weight: 600; padding: 3px 0 5px; }
  #note { font-size: 13px; color: #374151; margin: 8px 0 0; min-height: 36px; }
</style>
</head>
<body>
<div class="row">
  <label>min <input type="number" id="min" value="1"></label>
  <label>max <input type="number" id="max" value="6"></label>
  <select id="formula">
    <option value="floor">floor + 1 (correct)</option>
    <option value="round">Math.round (edges too rare)</option>
    <option value="noplus">floor, no + 1 (max never comes)</option>
  </select>
</div>
<code id="code"></code>
<div class="row" style="margin-top:10px">
  <button id="roll">Roll once</button> <span id="one">-</span>
  <button id="many" class="alt">Roll 10,000 times</button>
</div>
<div id="chart"></div>
<p id="note">Each bar counts how often that number came up.</p>

<script>
  const $ = (id) => document.getElementById(id);

  // the three versions people write; only the first is right
  const formulas = {
    floor:  (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
    round:  (min, max) => Math.round(Math.random() * (max - min)) + min,
    noplus: (min, max) => Math.floor(Math.random() * (max - min)) + min,
  };
  const text = {
    floor:  (a, b) => `Math.floor(Math.random() * (${b} - ${a} + 1)) + ${a}`,
    round:  (a, b) => `Math.round(Math.random() * (${b} - ${a})) + ${a}`,
    noplus: (a, b) => `Math.floor(Math.random() * (${b} - ${a})) + ${a}`,
  };

  function range() {
    let min = Math.ceil(Number($('min').value)), max = Math.floor(Number($('max').value));
    if (max < min) [min, max] = [max, min];
    if (max - min > 19) max = min + 19;  // keep the chart readable: at most 20 bars
    $('min').value = min; $('max').value = max;
    return [min, max];
  }

  function showCode() {
    const [min, max] = range();
    $('code').textContent = text[$('formula').value](min, max);
  }

  $('roll').addEventListener('click', () => {
    const [min, max] = range();
    $('one').textContent = formulas[$('formula').value](min, max);
  });

  $('many').addEventListener('click', () => {
    const [min, max] = range();
    const f = formulas[$('formula').value];
    const counts = {};
    for (let v = min; v <= max; v++) counts[v] = 0;
    for (let i = 0; i < 10000; i++) counts[f(min, max)]++;

    const n = max - min + 1, fair = 10000 / n, top = Math.max(...Object.values(counts), fair);
    $('chart').innerHTML = '';
    for (let v = min; v <= max; v++) {
      const bar = document.createElement('div');
      bar.className = 'bar';
      const low = counts[v] < fair * 0.8;  // well under the fair share
      bar.innerHTML = `<span class="cnt">${counts[v]}</span>
        <div class="fill${low ? ' low' : ''}" style="height:${counts[v] / top * 80}%"></div>
        <span class="lab">${v}</span>`;
      $('chart').append(bar);
    }
    $('note').textContent = `Fair share: about ${Math.round(fair)} per number. ` +
      (Object.values(counts).some((c) => c < fair * 0.8) ? 'Orange bars came up far less often.' : 'All bars are close to it.');
  });

  ['min', 'max', 'formula'].forEach((id) => $(id).addEventListener('change', () => { showCode(); $('many').click(); }));
  showCode();
  $('many').click();
</script>
</body>
</html>
10,000 rolls per chart. The correct formula gives bars of about the same height. Math.round starves the two ends, and forgetting + 1 means max never appears.

What Math.random() actually returns

Math.random() returns a decimal number from 0 up to, but not including, 1. It can return exactly 0, but never 1. Every other random value in this guide is built from that one number by stretching, cutting and shifting it.

You want Code Range
A fraction Math.random() 0 to just under 1
A decimal between a and b Math.random() * (b - a) + a a to just under b
A whole number below n Math.floor(Math.random() * n) 0 to n - 1
A whole number from min to max Math.floor(Math.random() * (max - min + 1)) + min min to max
true or false Math.random() < 0.5 each half the time

The numbers are "pseudo-random": the browser computes them from a hidden starting state. They are fine for games, demos and shuffling a quiz. They are not designed to be unguessable, which matters later.

A random integer between min and max

The formula reads from the inside out. Each step changes the range, and the last one lands exactly on min to max.

For a dice roll: stretch 0-1 to 0-6, cut the decimals to get 0-5, then add 1.
For a dice roll: stretch 0-1 to 0-6, cut the decimals to get 0-5, then add 1.
  1. Math.random() gives a fraction from 0 up to 1.
  2. * (max - min + 1) stretches it. The + 1 is there because 1 to 6 contains six numbers, not five.
  3. Math.floor cuts off the decimals, giving 0 to max - min.
  4. + min shifts the result so it starts at min.

It works for negative values as well. -3 to 3 gives seven results, and the demo above shows all seven equally often.

If min and max come from a text box, they arrive as strings. Wrap them in Number() first, otherwise + min joins text instead of adding.

Two mistakes that change the odds

Forgetting the + 1. Math.floor(Math.random() * (max - min)) + min looks right but can never return max. Math.random() never reaches 1, so the product never reaches the top of the range.

Using Math.round. Math.round(Math.random() * (max - min)) + min does return both ends, but only half as often as the middle. Rounding gives each end half a slice of the number line.

Math.floor gives six equal slices. Math.round gives the two end numbers half a slice each.
Math.floor gives six equal slices. Math.round gives the two end numbers half a slice each.

Pick the second option in the first demo and the orange bars show it: with 1 to 6, the ends land near 1,000 out of 10,000 rolls, the middle numbers near 2,000.

Pick a random item and shuffle an array

A random item from an array is the "whole number below n" row of the table, used as an index:

const colours = ['red', 'green', 'blue'];
const pick = colours[Math.floor(Math.random() * colours.length)];

To pick several items without repeats, remove each one as you pick it with splice, or shuffle a copy of the array and take the first few.

For shuffling, the one-liner that turns up everywhere is arr.sort(() => Math.random() - 0.5). It is short, and it is not fair. The demo shuffles four letters 20,000 times with both methods and counts where each letter ends up.

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>Fisher-Yates vs sort shuffle</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .top { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; font-size: 14px; }
  button { padding: 8px 14px; font-size: 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  .wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  @media (max-width: 520px) { .wrap { grid-template-columns: 1fr; } }
  .panel { background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px; }
  h3 { margin: 0 0 2px; font-size: 14px; }
  .sub { margin: 0 0 8px; font: 12px ui-monospace, Consolas, monospace; color: #5b6270; }
  table { border-collapse: collapse; width: 100%; font-size: 12px; }
  th { font-weight: 600; color: #5b6270; padding: 3px; }
  td { text-align: center; padding: 7px 2px; border: 2px solid #fff; border-radius: 4px; font-variant-numeric: tabular-nums; }
  .verdict { font-size: 12px; margin: 8px 0 0; }
</style>
</head>
<body>
<div class="top">
  <button id="run">Shuffle [A, B, C, D] 20,000 times</button>
  <span>Fair = 25% in every cell</span>
</div>
<div class="wrap">
  <div class="panel"><h3>Fisher-Yates</h3><p class="sub">swap from the end</p><table id="fy"></table><p class="verdict" id="fyv"></p></div>
  <div class="panel"><h3>sort(() =&gt; Math.random() - 0.5)</h3><p class="sub">random comparator</p><table id="st"></table><p class="verdict" id="stv"></p></div>
</div>

<script>
  // Fisher-Yates: walk from the end, swap each item with a random earlier (or same) one
  function shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
  }

  // the popular one-liner: short, but not every order is equally likely
  const sortShuffle = (arr) => arr.sort(() => Math.random() - 0.5);

  const ITEMS = ['A', 'B', 'C', 'D'], RUNS = 20000;

  function tally(fn) {
    // grid[item][position] = how many times that item landed there
    const grid = ITEMS.map(() => ITEMS.map(() => 0));
    for (let r = 0; r < RUNS; r++) {
      fn([...ITEMS]).forEach((item, pos) => grid[ITEMS.indexOf(item)][pos]++);
    }
    return grid;
  }

  function draw(tableId, verdictId, grid) {
    let worst = 0, html = '<tr><th></th>' + ITEMS.map((_, p) => `<th>pos ${p + 1}</th>`).join('') + '</tr>';
    grid.forEach((row, i) => {
      html += `<tr><th>${ITEMS[i]}</th>`;
      row.forEach((count) => {
        const pct = count / RUNS * 100, off = Math.abs(pct - 25);
        worst = Math.max(worst, off);
        // green near 25%, orange the further it drifts
        const bg = off < 2 ? '#dcfce7' : off < 6 ? '#fed7aa' : '#fb923c';
        html += `<td style="background:${bg}">${pct.toFixed(1)}%</td>`;
      });
      html += '</tr>';
    });
    document.getElementById(tableId).innerHTML = html;
    document.getElementById(verdictId).textContent = worst < 2
      ? `Every cell within 2 points of 25%.`
      : `Some cells are ${worst.toFixed(1)} points away from 25%.`;
  }

  document.getElementById('run').addEventListener('click', () => {
    draw('fy', 'fyv', tally(shuffle));
    draw('st', 'stv', tally(sortShuffle));
  });
  document.getElementById('run').click();
</script>
</body>
</html>
Each cell is how often a letter landed in that position. A fair shuffle keeps every cell near 25%.

A comparator that gives random answers breaks the rules sort expects, so the specification leaves the result up to each browser. The exact pattern of bias depends on the browser's sort algorithm, but it does not go away.

The fair method is the Fisher-Yates shuffle. Walk from the last position to the second, and swap each one with a random position at or before it:

function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // 0 to i, i included
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}
Each step picks from the items still in the pool, so each of the 24 orders of four items is reached by exactly one path.
Each step picks from the items still in the pool, so each of the 24 orders of four items is reached by exactly one path.

It shuffles in place. Use shuffle([...arr]) to keep the original. Note the i + 1: letting an item swap with itself is part of what makes it fair.

Random colours

A colour in hex is a number from 0 to 0xFFFFFF. Pick one and write it as six hex digits:

const hex = '#' + Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, '0');

padStart matters: small numbers would otherwise give five or fewer digits, which is not a valid colour. For colours that look good together, keep saturation and lightness fixed and randomise only the hue, as in hsl(${Math.floor(Math.random() * 360)} 70% 55%).

To apply a random colour across a page, set it on a custom property and let the CSS use it. CSS variables shows how setProperty updates every element that reads the variable.

When it has to be secure: the crypto API

Math.random() is not cryptographically secure. Its output was never meant to be unpredictable to someone trying to guess it. Do not use it for passwords, tokens, invite codes or anything that protects access.

Browsers provide a secure generator on the global crypto object:

  • crypto.getRandomValues(array) fills a typed array, such as Uint32Array, with secure random integers. One call can fill up to 65,536 bytes.
  • crypto.randomUUID() returns a random version 4 UUID, a 36-character ID such as 3b241101-e2bb-4255-8caf-4136c566a962. It needs a secure context: an https:// page or localhost.

Turning a random 32-bit integer into a range needs care. x % 6 favours the low numbers slightly, because 2^32 is not a multiple of 6. The fix is to throw away values from the last, incomplete block and draw again:

function secureInt(n) {                        // 0 to n - 1
  const limit = Math.floor(2 ** 32 / n) * n;   // largest multiple of n that fits
  const buf = new Uint32Array(1);
  do crypto.getRandomValues(buf); while (buf[0] >= limit);
  return buf[0] % n;
}

Repeatable random numbers with a seed

Sometimes you want the same "random" sequence every time: a daily puzzle, a test you can rerun, a generated level that looks the same for every player. Math.random() cannot do this, because it has no seed.

A seeded generator is a small function that turns a starting number into a sequence. Mulberry32 is a widely copied example, a few lines long:

function mulberry32(seed) {
  return function () {
    seed = (seed + 0x6D2B79F5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;  // 0 up to 1, like Math.random()
  };
}

const rand = mulberry32(20260926);
rand(); rand(); // the same two numbers on every run, in every browser

Use rand() anywhere this guide uses Math.random(). A seeded generator like this is for games and tests only, never for security.

For a puzzle that changes daily, build the seed from today's date; formatting dates in JavaScript covers getting the year, month and day as numbers.

A finished example: a random picker

This page combines the pieces. It draws winners without repeats, rolls dice, makes a colour palette and creates IDs. The checkbox switches every draw from Math.random() to crypto.getRandomValues() with the unbiased secureInt approach.

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>Random picker</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .mode { font-size: 14px; margin-bottom: 10px; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  @media (max-width: 560px) { .grid { grid-template-columns: 1fr; } }
  .card { background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; padding: 10px 12px; }
  h3 { margin: 0 0 8px; font-size: 14px; }
  textarea { width: 100%; box-sizing: border-box; height: 64px; font: 14px system-ui, sans-serif; padding: 6px; }
  button { padding: 7px 12px; font-size: 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  button.alt { background: #fff; color: #1d4ed8; border: 1px solid #1d4ed8; }
  button:disabled { background: #9aa3b2; }
  .out { font-size: 22px; font-weight: 700; margin: 6px 0 2px; min-height: 30px; }
  .small { font-size: 12px; color: #5b6270; margin: 4px 0 0; }
  .dice { font-size: 40px; line-height: 1.1; letter-spacing: 4px; min-height: 46px; }
  .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  select { padding: 5px; font-size: 14px; }
  .pal { display: flex; height: 64px; border-radius: 8px; overflow: hidden; margin-top: 8px; }
  .pal div { flex: 1; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 4px;
             font: 11px ui-monospace, Consolas, monospace; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.6); }
  #uuid { font: 12px ui-monospace, Consolas, monospace; word-break: break-all; margin: 8px 0 0; }
</style>
</head>
<body>
<label class="mode"><input type="checkbox" id="secure"> Use <b>crypto.getRandomValues</b> instead of Math.random</label>

<div class="grid">
  <div class="card">
    <h3>Pick a winner (no repeats)</h3>
    <textarea id="names">Ana
Ben
Chloe
Dev
Emi</textarea>
    <div class="row"><button id="pick">Pick</button><button id="reset" class="alt">Start over</button></div>
    <div class="out" id="winner"></div>
    <p class="small" id="picked">Picked so far: none</p>
  </div>

  <div class="card">
    <h3>Dice roller</h3>
    <div class="row">
      <select id="count"><option>1</option><option selected>2</option><option>3</option><option>4</option><option>5</option></select>
      <button id="roll">Roll</button> <span id="total" class="small"></span>
    </div>
    <div class="dice" id="dice"></div>
  </div>

  <div class="card">
    <h3>Random colour palette</h3>
    <button id="colors">New palette</button>
    <div class="pal" id="pal"></div>
  </div>

  <div class="card">
    <h3>Random ID</h3>
    <button id="id">crypto.randomUUID()</button>
    <p id="uuid"></p>
  </div>
</div>

<script>
  const $ = (id) => document.getElementById(id);

  // whole number from 0 to n - 1
  function randomInt(n) {
    if (!$('secure').checked) return Math.floor(Math.random() * n);
    // crypto version: throw away values above the last full block of n, so no number is favoured
    const limit = Math.floor(2 ** 32 / n) * n, buf = new Uint32Array(1);
    do crypto.getRandomValues(buf); while (buf[0] >= limit);
    return buf[0] % n;
  }

  // Winner: take the name out of the pool so it cannot come up again
  let pool = null, picked = [];
  function startOver() {
    pool = $('names').value.split('\n').map((s) => s.trim()).filter(Boolean);
    picked = [];
    $('winner').textContent = '';
    $('picked').textContent = 'Picked so far: none';
    $('pick').disabled = pool.length === 0;
  }
  $('pick').addEventListener('click', () => {
    if (pool === null) startOver();
    const [name] = pool.splice(randomInt(pool.length), 1);
    picked.push(name);
    $('winner').textContent = name;
    $('picked').textContent = 'Picked so far: ' + picked.join(', ') + (pool.length ? '' : ' (everyone)');
    $('pick').disabled = pool.length === 0;
  });
  $('reset').addEventListener('click', startOver);
  $('names').addEventListener('input', startOver);

  // Dice: each die is a random integer from 1 to 6
  const FACES = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
  $('roll').addEventListener('click', () => {
    const rolls = Array.from({ length: Number($('count').value) }, () => randomInt(6) + 1);
    $('dice').textContent = rolls.map((r) => FACES[r - 1]).join('');
    $('total').textContent = rolls.join(' + ') + ' = ' + rolls.reduce((a, b) => a + b, 0);
  });

  // Palette: a random number from 0 to 0xFFFFFF, written as 6 hex digits
  $('colors').addEventListener('click', () => {
    $('pal').innerHTML = '';
    for (let i = 0; i < 5; i++) {
      const hex = '#' + randomInt(0x1000000).toString(16).padStart(6, '0');
      const sw = document.createElement('div');
      sw.style.background = hex;
      sw.textContent = hex;
      $('pal').append(sw);
    }
  });

  // IDs always come from crypto, whatever the checkbox says
  $('id').addEventListener('click', () => { $('uuid').textContent = crypto.randomUUID(); });

  startOver();
  $('roll').click();
  $('colors').click();
  $('id').click();
</script>
</body>
</html>
Draw names until everyone has been picked, roll up to five dice, and generate palettes and IDs. Tick the box to use the crypto generator.
  • No repeats: each pick removes the name with splice, so the pool shrinks until the button turns off.
  • One helper: every feature calls randomInt(n), so switching the generator is one if.
  • IDs: crypto.randomUUID() is used whatever the checkbox says, because IDs should not be guessable.

To roll automatically every few seconds, or add a short "rolling" animation before the result, see setTimeout and setInterval.

When it does not work

What you see Cause Fix
max never comes up Math.floor without + 1 Multiply by (max - min + 1)
min and max come up half as often as the others Math.round instead of Math.floor Use Math.floor and + 1
Results such as "41" instead of 5 min and max are strings from an input Convert with Number() first
Some shuffled orders keep showing up sort(() => Math.random() - 0.5) Use Fisher-Yates
Colour codes with five digits Missing padStart(6, '0') Pad the hex string
You expected the same sequence twice Math.random() cannot be seeded Use a seeded generator such as mulberry32
Tokens or passwords made with Math.random It is not cryptographically secure Use crypto.getRandomValues or crypto.randomUUID
crypto.randomUUID is not a function The page is not a secure context Serve it over https:// or localhost

A random picker is more convincing when the other person can press the button. A screenshot only shows one result, and an .html attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can roll the dice and draw names themselves. If you change the code later, the same link shows the new version.

Questions people ask

How do I get a random integer between two numbers in JavaScript?

Use Math.floor(Math.random() * (max - min + 1)) + min. Both min and max can come out, every whole number in between has the same chance, and it works with negative numbers too.

Can Math.random() return 1?

No. It returns a number from 0 up to, but not including, 1. It can return 0. That is why Math.floor(Math.random() * n) never reaches n, and why the formula adds 1 to reach max.

Is Math.random() safe for passwords or tokens?

No. Math.random() does not provide cryptographically secure numbers. For anything someone should not be able to guess, use crypto.getRandomValues() or crypto.randomUUID(), which the browser backs with a secure generator.

Can I seed Math.random() to get the same numbers again?

No. Math.random() has no seed parameter; the browser picks the starting state and you cannot set or reset it. For repeatable sequences, write or copy a small seeded generator such as mulberry32 and call it instead.

Why is sort(() => Math.random() - 0.5) a bad shuffle?

A comparator that answers randomly is not consistent, and the specification leaves the resulting order up to the implementation. In practice some orders come out more often than others. The Fisher-Yates shuffle gives every order the same chance.

Keep reading