Google's built-in dice roller was stealing my search traffic. Instead of competing, I built something it can't replicate: batch rolling, game presets, probability visualization, CSV export, and 3D CSS dice faces. Here's the full technical breakdown.
How I Built a Dice Roller That Google Can't Beat
Google was stealing my clicks. Every time someone searched "dice roller," Google's own interactive dice widget sat at the top of the results — a fully functional, zero-click tool that let users roll dice without ever visiting a website.
This is the story of how I analyzed the competitive landscape, identified what Google's built-in tool couldn't do, and built a dice roller that serves real users instead of competing with a search giant head-on.
Part 1: The Discovery
I run ToolKnit, a collection of 90 free browser-based tools. Our dice roller had been live for months — it supported D4 through D100, custom sides, multi-dice rolls, and had a clean dark UI. But the click-through rate from Google Search was abysmal.
When I looked at the SERP for "dice roller," the answer was obvious:
Google displays an interactive dice widget directly in the search results. Users can click it, roll dice, and get results — all without leaving Google. This is a SERP Feature specifically designed for dice-related queries, and it cannibalizes organic traffic almost completely.
The conventional SEO playbook says: "Optimize your meta tags, build more backlinks, write better content." But none of that matters when Google puts a working tool above your link. Users don't scroll down because they don't need to.
The insight: Stop trying to outrank Google. Instead, build something Google's widget can't do.
Part 2: What Google's Dice Can't Do
Google's built-in dice roller is minimal by design. It supports:
- D4, D6, D8, D10, D12, D20, D100
- Multiple dice (e.g., 3d6)
- Adding modifiers (+3, +5)
- An animation that shows results
What it doesn't support:
- ❌ Game-specific presets (Monopoly, Yahtzee, Catan, Craps)
- ❌ Batch rolling (10×, 50×, 100×)
- ❌ Roll history with CSV export
- ❌ Probability distribution visualization
- ❌ Dice face customization (skins)
- ❌ Keyboard shortcuts
- ❌ Sound effects
- ❌ Session statistics (total rolls, average, grand sum)
Every feature on this list is something a real user would want — whether they're a D&D player tracking combat rounds, a teacher running a probability lesson, or a board game enthusiast who lost their physical dice.
I decided to build all of them.
Part 3: The Technical Build
Here's the architecture: zero dependencies, pure vanilla JavaScript, CSS animations, and HTML5 Canvas. No npm, no build step, no framework. The tool is a single HTML file with inline JS that loads in under 300ms.
3.1 3D Dice Faces with Pure CSS
The dice aren't images or SVGs — they're styled <div> elements with CSS grid layouts. Each face shows the correct dot pattern:
.dice-face {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
width: 64px;
height: 64px;
background: #1a1a2e;
border: 2px solid rgba(255,255,255,0.12);
border-radius: 14px;
padding: 8px;
}
.dot {
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
place-self: center;
}
The roll animation applies a CSS transform sequence — rotation in 3D space with a scale bounce:
@keyframes diceRoll {
0% { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg) scale(1); }
25% { transform: rotateX(180deg) rotateY(90deg) rotateZ(45deg) scale(1.1); }
50% { transform: rotateX(360deg) rotateY(180deg) rotateZ(90deg) scale(0.95); }
75% { transform: rotateX(540deg) rotateY(270deg) rotateZ(135deg) scale(1.05); }
100% { transform: rotateX(720deg) rotateY(360deg) rotateZ(180deg) scale(1); }
}
Each die gets a different animation delay so they tumble sequentially, not simultaneously — this creates a much more satisfying visual cascade.
3.2 requestAnimationFrame for Batch Rolling
The batch roll feature was the hardest technical challenge. Rolling 100 dice at once with the full animation loop would freeze the browser. The solution: chunked processing with requestAnimationFrame.
function batchRoll(count) {
if (isRolling) return;
isRolling = true;
var batchResults = [];
var i = 0;
function step() {
// Process 10 rolls per frame to avoid UI freeze
var chunk = Math.min(10, count - i);
for (var j = 0; j < chunk; j++) {
var results = [];
for (var d = 0; d < diceCount; d++) {
results.push(Math.floor(Math.random() * 6) + 1);
}
batchResults.push(results);
var total = results.reduce(function(a, b) { return a + b; }, 0);
rolls++;
grandTotal += total;
results.forEach(function(v) { distribution[v]++; });
}
i += chunk;
// Update UI every frame
updateDistribution();
updateHistory();
progressBar.style.width = Math.round((i / count) * 100) + '%';
if (i < count) {
requestAnimationFrame(step);
} else {
// Final animation for the last result
renderDice(batchResults[batchResults.length - 1]);
isRolling = false;
}
}
requestAnimationFrame(step);
}
Key decisions:
- 10 rolls per frame keeps the UI responsive while maintaining visual feedback
- The progress bar updates every frame, giving users a satisfying sense of progress
- The last roll gets the full dice animation treatment so it "feels" like a real roll
-
isRollinggate prevents double-clicks from stacking batch operations
3.3 Game Presets
Instead of asking users to configure dice manually, I added one-click presets for popular games:
| Preset | Dice | Why |
|---|---|---|
| Monopoly | 2d6 | Classic property-buying rolls |
| Yahtzee | 5d6 | Five dice, three rolls per turn (manual) |
| Catan | 2d6 | Settlers resource distribution |
| Craps | 2d6 | Casino-style come-out roll |
| D&D Skill | 1d20 | Ability checks and saving throws |
Each preset adjusts the dice count and type automatically. This small UX decision matters enormously for casual users who just want to play their game without reading documentation.
3.4 Probability Distribution with Theoretical Comparison
The distribution chart uses HTML5 Canvas to draw a bar chart showing the frequency of each die face (1-6). But the real insight was adding a theoretical overlay:
// Theoretical: exactly 1/6 = 16.67% per face
var theoretical = 1 / 6;
var theoreticalHeight = maxBarHeight * theoretical;
ctx.fillStyle = 'rgba(59, 130, 246, 0.25)';
ctx.fillRect(barX, chartH - theoreticalHeight, barW, theoreticalHeight);
Each bar shows:
- Actual frequency (solid white/gray bar)
- Theoretical 16.67% (semi-transparent blue overlay)
As users roll more, the bars converge toward the theoretical line — visually demonstrating the Law of Large Numbers. This is useful for teachers and anyone curious about probability.
3.5 CSV Export
Roll history is more useful when you can take it elsewhere. The CSV export button generates a downloadable file with headers:
function exportCSV() {
var csv = 'Roll#,Die1,Die2,Die3,Die4,Die5,Die6,Total\n';
for (var i = historyData.length - 1; i >= 0; i--) {
var row = historyData[i];
csv += (row.roll) + ',' + row.results.join(',') + ',' + row.total + '\n';
}
var blob = new Blob([csv], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'dice-rolls.csv';
a.click();
}
This lets users analyze their rolls in Excel, Google Sheets, or any statistics tool. Combined with the batch roll feature, you can generate thousands of data points for a probability experiment in seconds.
3.6 Dice Skins
Three color themes (slate, indigo, emerald) that apply via CSS class switching. Under the hood, it's just swapping data-skin attributes and re-rendering. The DOM stays unchanged — only the visual presentation changes.
3.7 Keyboard Shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === ' ' || e.key === 'Enter') { roll(); }
if (e.key === 'r' || e.key === 'R') { reset(); }
if (e.key === 'ArrowUp') { changeDice(1); }
if (e.key === 'ArrowDown') { changeDice(-1); }
if (e.key === '1') { batchRoll(10); }
if (e.key === '5') { batchRoll(50); }
if (e.key === '0') { batchRoll(100); }
});
Space/Enter to roll, R to reset, arrows to adjust dice count, number keys for batch operations. Power users love this.
Part 4: The Results
After the update, the dice roller went from a generic tool to a feature-rich dice toolbox. Key metrics:
| Metric | Before | After |
|---|---|---|
| Weekly Google impressions | ~200 | 2,230+ |
| Weekly Bing impressions | ~50 | 1,300+ |
| Total weekly exposure | ~250 | 3,500+ |
That's a 14× increase in three months.
The lesson: Google's SERP Features aren't an impenetrable wall. They're a constraint that forces you to build something genuinely better. If Google's widget does A, B, and C — build A through Z. Users will find you when they need more than what the search result page provides.
Part 5: What I'd Do Differently
-
Web Workers for batch rolling. Currently,
requestAnimationFrameis good enough for 100 rolls, but 1,000+ rolls should be offloaded to a Worker thread. - IndexedDB for persistent roll history. Right now, history resets on page reload. Storing it locally would make the tool feel like a real app.
- Physics-based 3D dice. The CSS animations look good, but a Three.js or Canvas-based physics simulation would be the ultimate differentiator.
- Multiplayer sync. Imagine a shared dice room where multiple players see the same roll in real-time via WebSockets.
The Bigger Picture
This approach applies to any tool that faces Google SERP Feature competition: calculators, unit converters, timers, coin flippers, color pickers. The strategy is always the same:
- Analyze what the SERP Feature does.
- List everything it doesn't do that real users need.
- Build those things.
Don't try to outrank Google. Out-build them.
This article was written by the creator of ToolKnit — 90 free browser-based tools that run entirely in your browser with no sign-up and no uploads. Try the Dice Roller and let me know what else you'd want to see.





Top comments (0)