Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Dice Roller Dialog Block for React

A game-night app's character sheet with its dice tray shown open: six drawn dice from d4 to d20, a count and a bonus, advantage and disadvantage on the d20, faces that tumble in, the total as a sum and a history of the last eight rolls.

Framework
UI library

Preview

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Overview

Roll for the thing your character just tried, without finding a physical die, with this dice roller dialog for React with Base UI. It opens from the character sheet of Hexhall, a game-night app: the header holds an outline Button, and the sheet it opens over is drawn under a soft scrim inside a hairline frame with a plus mark on each corner, with a header cell for the last total and six stat rows. The Dialog picks the die with a Toggle Group of six drawn polygons, d4 to d20, takes how many (1 to 8) and a bonus (-5 to +5) in two Number Inputs, and for the d20 offers a Segment Group, Normal, Advantage or Disadvantage, that rolls two and keeps one. Roll, the only Shine Button, draws the faces, which tumble in, reads the total as a sum such as 2d6 + 3 = 14, and adds it to a list of the last eight rolls with a Clear button. The dice are seeded: a small generator draws the stream, so a reload repeats the same rolls in the same order. For preview the dialog is drawn open in place when the page loads; the header button opens the real Dialog, with the same state. For tabletop and board-game companions, classroom probability and any app with a roll in it.

FAQ

Why is the dialog already open, and what do I keep?

So you can see it without a click. On load the dialog is a copy drawn in place over the character sheet: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Roll dice button in the header opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same TrayFields component and share the block's state, so what you set in the copy is there in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.

Are the dice really random?

No, and the dialog says so. streamOf runs a small seeded generator (Park-Miller, seed 4,817) once at module scope and keeps 256 draws between 0 and 1. A roll reads the next draws, turns each into a face with 1 + floor(draw x sides), and moves the cursor on, so a reload repeats the same rolls in the same order. That is right for a demo. For a roll that matters, take the faces from crypto.getRandomValues or your server inside rollOf.

How do advantage and disadvantage work?

They belong to the d20. With Advantage or Disadvantage chosen, the roll draws two d20, keeps the higher or the lower, adds the bonus, and shows the set-aside face struck through and named in text for screen readers. The how-many field is then disabled and reads 2, since two dice are always rolled. For any other die the option row keeps its place but is hidden, and a sentence says why.

What happens with reduced motion?

Each face enters with a 500 ms CSS transition from a quarter turn, a little smaller and transparent, and a new roll replaces the faces so they tumble in again. With reduced motion on, the transition is off and the faces just appear. Nothing loops and nothing runs a frame loop. The faces, the total and the history are all text; the polygons behind the numbers are decoration.

What stays local, and how do I wire it for real?

Everything. The header cell of the drawn sheet shows the last total, and nothing is sent. A refused roll, such as 12 dice or a bonus of 9, puts its problem in one reserved line and focuses the count field; the numbers are validated by wholeIn, not trusted from the field. In your app post each roll from roll to your table's shared log so everyone sees it. Closing the dialog keeps the tray and the history.

Is it accessible from the keyboard?

Yes. The real dialog is named by its title and described by its sentence, focus is trapped while it is open, and Escape and the close button return focus to the header button. The trigger is passed to DialogTrigger through render, and the dialog is controlled with open and onOpenChange. The copy shown on load moves no focus; Escape inside it and its close button close it and put focus back on the header button. The dice are a toggle group moved with the arrow keys, Enter in either number field rolls, and the history is a scrollable region that takes Tab.

How does it look on a phone?

The copy shown on load docks to the foot of the frame as a bottom sheet with Close and Roll pinned below a scrolling body. The six dice share one row, and the tray and the history keep their height whether they hold rolls or not. The real dialog keeps the Dialog's own phone layout: a sheet stuck to the bottom of the screen.