A dice roller is only worth using if the numbers are honest, so here is exactly how the numbers on this site are produced — including the code that produces them. There are three things worth explaining: where the randomness comes from, how it is fitted to the die without introducing bias, and what the animation does and does not do.
Browsers ship two random number generators. Math.random() is the familiar one: fast, deterministic, seeded once, and explicitly not required by its specification to be unpredictable. It is adequate for shuffling a carousel and it is what most dice rollers use.
This site uses the other one. crypto.getRandomValues() draws from the operating system's entropy pool — hardware noise, timing jitter, and on most modern machines a dedicated CPU instruction — and is required to be unpredictable even to someone who has seen every previous output. It is the generator browsers use to create encryption keys. Using it for dice is not necessary, exactly, but it is one line of code and it removes an entire category of doubt.
Good randomness is only half the problem. The generator hands back a number spread across a huge range, and it has to be squeezed into 1–6 or 1–20 or 1–100. The obvious way to do that is the remainder operator, and the obvious way is subtly wrong.
Suppose you have a random byte — 256 possible values — and you want a d6. 256 divided by 6 is 42 remainder 4, so four of the six faces get 43 chances and two get only 42. Those faces come up 16.80% of the time against 16.41%. Small, but permanent, and no quality of randomness upstream will repair it.
The same mistake on a d100 is not small at all. 256 divided by 100 is 2 remainder 56, so results 1 to 56 get three chances and 57 to 100 get two — low numbers become 50% more likely than high ones. In a roll-under system, that is a broken game rather than a rounding error.
The fix is rejection sampling: work out the largest multiple of the die size that fits in the generator's range, discard anything above it, and draw again. Discarded draws are rare and the loop is imperceptible, but the result is that every face is exactly equally likely rather than nearly so.
This is the whole of it — the function every roll on the site goes through, copied verbatim from src/lib/dice.ts:
/** Cryptographically secure random integer in [1, max], unbiased via rejection sampling. */
export function secureRollDie(max: number): number {
const range = max;
const maxUint32 = 0xffffffff;
const limit = maxUint32 - (maxUint32 % range);
const buf = new Uint32Array(1);
let value: number;
do {
crypto.getRandomValues(buf);
value = buf[0];
} while (value >= limit);
return (value % range) + 1;
}limit is the largest multiple of the die size that fits inside a 32-bit unsigned integer. Anything at or above it is thrown away and redrawn, which is the do…while loop. What survives is reduced with the remainder and shifted up by one, giving a result from 1 to the number of faces, with every face exactly as likely as every other.
Rolling several dice calls this function once per die. There is no shared state between rolls, no seed carried forward, and no adjustment based on what came before — a run of low results makes the next roll no more likely to be high, which is how randomness is supposed to behave even though it rarely feels like it.
The dice you see are rendered with WebGL, and they tumble convincingly, but the physics does not decide anything. The order of events is: draw the number from the secure generator, then tell the die which face to land on, then animate it getting there.
That ordering is deliberate and it is the fairer arrangement, not the lazier one. A simulated tumble read off the resting face would inherit the biases of a floating-point physics engine, and would still need an answer for the die that lands on its point or slips through the table. Drawing the number first means the result depends on one well-understood generator instead of a simulation nobody has audited.
One consequence worth stating plainly: the roller has no way to favour or disfavour any result. There is no code path that inspects a result before displaying it.
Rolls are generated in your browser, displayed in your browser, and stored — if at all — in your browser's local storage, which is where the roll history and your settings live. No roll is sent to a server, because there is no server involved in rolling: the site is a set of static files.
The practical effect is that once the page has loaded, the roller keeps working without a connection, and clearing your browser data clears everything the site knows about you. The privacy policy covers the third-party advertising and analytics that fund the site, which are the only places any data goes.
You do not have to take any of this on faith. Roll a die a few thousand times and count the results — if a d20 face turns up 700 times in 10,000 rolls where 500 is expected, something is wrong. What you cannot do is conclude anything from twenty rolls or from one bad evening: randomness produces streaks, and human intuition consistently expects results to be more evenly spread than they really are.
For the wider question of how digital rolls compare to the plastic dice in your bag — which are less fair than most people assume — see are online dice rollers actually random?
Last updated: August 20, 2026