Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Dice Roller
Dice roller dialog with multiple dice types, quantity selection, roll animation, history log, and total calculation
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Roll the dice with satisfying animations. This React dice roller dialog provides multiple dice types (D4, D6, D8, D10, D12, D20), quantity selection, smooth roll animations, roll history logging, and automatic total calculation. Built with shadcn/ui Dialog, Button, Badge, Select, and ScrollArea components using Tailwind CSS, users roll virtual dice with visual feedback and statistical tracking. Select dice, set quantity, roll with animation—perfect for tabletop gaming apps, RPG companions, board game tools, or any Next.js application where random dice rolling adds excitement and fair chance mechanics.
"use client";import { useState, useCallback } from "react";import { Dices, RotateCcw, History, Plus, Minus, Sparkles, Trash2,} from "lucide-react";import { Button } from "@/components/ui/button";import { Badge } from "@/components/ui/badge";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { ScrollArea } from "@/components/ui/scroll-area";import { Separator } from "@/components/ui/separator";import { cn } from "@/lib/utils";type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20";type RollResult = { id: string; diceType: DiceType; quantity: number; results: number[]; total: number; timestamp: Date; isCritical: boolean; isCriticalFail: boolean;};const diceTypes: { type: DiceType; sides: number; label: string }[] = [ { type: "d4", sides: 4, label: "D4" }, { type: "d6", sides: 6, label: "D6" }, { type: "d8", sides: 8, label: "D8" }, { type: "d10", sides: 10, label: "D10" }, { type: "d12", sides: 12, label: "D12" }, { type: "d20", sides: 20, label: "D20" },];export const title = "React Dialog Block Dice Roller";export default function DialogDiceRoller() { const [open, setOpen] = useState(false); const [selectedDice, setSelectedDice] = useState<DiceType>("d20"); const [quantity, setQuantity] = useState(1); const [isRolling, setIsRolling] = useState(false); const [currentResult, setCurrentResult] = useState<RollResult | null>(null); const [history, setHistory] = useState<RollResult[]>([]); const [showHistory, setShowHistory] = useState(false); const getDiceSides = (type: DiceType) => { return diceTypes.find((d) => d.type === type)?.sides || 6; }; const rollDice = useCallback(() => { const sides = getDiceSides(selectedDice); setIsRolling(true); // Animate with random values let animationCount = 0; const animationInterval = setInterval(() => { const tempResults = Array.from({ length: quantity }, () => Math.floor(Math.random() * sides) + 1 ); setCurrentResult({ id: "temp", diceType: selectedDice, quantity, results: tempResults, total: tempResults.reduce((a, b) => a + b, 0), timestamp: new Date(), isCritical: false, isCriticalFail: false, }); animationCount++; if (animationCount >= 10) { clearInterval(animationInterval); // Final roll const finalResults = Array.from({ length: quantity }, () => Math.floor(Math.random() * sides) + 1 ); const total = finalResults.reduce((a, b) => a + b, 0); const isCritical = selectedDice === "d20" && finalResults.includes(20); const isCriticalFail = selectedDice === "d20" && finalResults.includes(1); const result: RollResult = { id: Date.now().toString(), diceType: selectedDice, quantity, results: finalResults, total, timestamp: new Date(), isCritical, isCriticalFail, }; setCurrentResult(result); setHistory((prev) => [result, ...prev.slice(0, 19)]); setIsRolling(false); } }, 50); }, [selectedDice, quantity]); const handleQuantityChange = (delta: number) => { setQuantity((prev) => Math.max(1, Math.min(10, prev + delta))); }; const clearHistory = () => { setHistory([]); }; const formatTime = (date: Date) => { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Dices className="mr-2 h-4 w-4" /> Roll Dice </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Dices className="h-5 w-5" /> Dice Roller </div> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowHistory(!showHistory)} > <History className="h-4 w-4" /> </Button> </DialogTitle> <DialogDescription> Select dice type and quantity, then roll! </DialogDescription> </DialogHeader> {showHistory ? ( <div className="space-y-3 py-4"> <div className="flex items-center justify-between"> <span className="text-sm font-medium">Roll History</span> {history.length > 0 && ( <Button variant="ghost" size="sm" onClick={clearHistory} className="h-7 text-xs" > <Trash2 className="mr-1 h-3 w-3" /> Clear </Button> )} </div> <ScrollArea className="h-[300px]"> {history.length === 0 ? ( <div className="flex flex-col items-center justify-center py-8 text-center"> <History className="h-8 w-8 text-muted-foreground mb-2" /> <p className="text-sm text-muted-foreground">No rolls yet</p> </div> ) : ( <div className="space-y-2"> {history.map((roll) => ( <div key={roll.id} className={cn( "rounded-lg border p-3", roll.isCritical && "border-primary bg-primary/5", roll.isCriticalFail && "border-destructive bg-destructive/5" )} > <div className="flex items-center justify-between mb-1"> <Badge variant="outline"> {roll.quantity}{roll.diceType.toUpperCase()} </Badge> <span className="text-xs text-muted-foreground"> {formatTime(roll.timestamp)} </span> </div> <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground"> [{roll.results.join(", ")}] </span> <span className="font-mono font-bold">= {roll.total}</span> </div> {roll.isCritical && ( <Badge className="mt-1" variant="default"> Critical Hit! </Badge> )} {roll.isCriticalFail && ( <Badge className="mt-1" variant="destructive"> Critical Fail! </Badge> )} </div> ))} </div> )} </ScrollArea> <Button variant="outline" className="w-full" onClick={() => setShowHistory(false)} > Back to Roller </Button> </div> ) : ( <div className="space-y-4 py-4"> <div className="grid grid-cols-6 gap-2"> {diceTypes.map((dice) => ( <Button key={dice.type} variant={selectedDice === dice.type ? "default" : "outline"} size="sm" className="font-mono" onClick={() => setSelectedDice(dice.type)} > {dice.label} </Button> ))} </div> <div className="flex items-center justify-between"> <span className="text-sm font-medium">Quantity</span> <div className="flex items-center gap-2"> <Button variant="outline" size="icon" className="h-8 w-8" onClick={() => handleQuantityChange(-1)} disabled={quantity <= 1} > <Minus className="h-4 w-4" /> </Button> <span className="w-8 text-center font-mono font-bold text-lg"> {quantity} </span> <Button variant="outline" size="icon" className="h-8 w-8" onClick={() => handleQuantityChange(1)} disabled={quantity >= 10} > <Plus className="h-4 w-4" /> </Button> </div> </div> <div className="text-center py-2"> <Badge variant="secondary" className="text-lg font-mono"> {quantity}{selectedDice.toUpperCase()} </Badge> </div> <Separator /> {currentResult && ( <div className={cn( "rounded-lg border p-4 text-center transition-all", isRolling && "animate-pulse", currentResult.isCritical && "border-primary bg-primary/5", currentResult.isCriticalFail && "border-destructive bg-destructive/5" )} > {currentResult.isCritical && !isRolling && ( <div className="flex items-center justify-center gap-1 text-primary mb-2"> <Sparkles className="h-4 w-4" /> <span className="text-sm font-medium">Natural 20!</span> <Sparkles className="h-4 w-4" /> </div> )} {currentResult.isCriticalFail && !isRolling && ( <div className="text-destructive text-sm font-medium mb-2"> Natural 1! </div> )} <div className="text-4xl font-mono font-bold tabular-nums"> {currentResult.total} </div> {currentResult.quantity > 1 && ( <div className="text-sm text-muted-foreground mt-2"> [{currentResult.results.join(" + ")}] </div> )} </div> )} {!currentResult && ( <div className="rounded-lg border border-dashed p-4 text-center"> <p className="text-sm text-muted-foreground"> Click roll to get started </p> </div> )} <Button className="w-full" size="lg" onClick={rollDice} disabled={isRolling} > {isRolling ? ( <RotateCcw className="mr-2 h-5 w-5 animate-spin" /> ) : ( <Dices className="mr-2 h-5 w-5" /> )} {isRolling ? "Rolling..." : "Roll Dice"} </Button> {history.length > 0 && ( <p className="text-xs text-center text-muted-foreground"> {history.length} roll{history.length !== 1 ? "s" : ""} in history </p> )} </div> )} </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-dice-roller.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-dice-roller.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-dice-roller.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-dice-roller.jsonRelated blocks you will also like
React Dialog Block Quick Calculator
Number calculations
React Dialog Block Coin Flip
Random decisions
React Dialog Block Password Generator
Random generation
React Dialog Block Poll Create
Game voting
React Dialog Block Success Confirmation
Roll results
React Dialog Block Keyboard Shortcuts
Quick rolls
Questions you might have
React Dialog Block Device Management
Device management dialog with active sessions list, device details, location info, last active timestamps, and remote logout functionality
React Dialog Block Download App
Download app dialog with platform detection, QR code display, app store links, and installation instructions