Making dice with CSS 3D
Preamble
Over my Easter break I planned to add a random number generator tool to my site to expand the utility tools and as a fun side project.
Putting in a Math.random() mapped to a configurable range was a quick and easy start, but I knew that I wanted to do something more interesting than just a number in a box.
I haven’t done much geometry or trigonometry on paper since uni, and nowadays I would usually reach for a 3D library to handle the math, but given I have the time, I thought it would be fun to work it out myself and build a CSS-only 3D dice roller.
I couldn’t find any existing CSS examples that positioned the faces of the polyhedra using the mathematical definitions, so I took the time to work it out from scratch.
The constraint I set: CSS-only 3D — no canvas, no WebGL, no three.js. JavaScript only picks a random number and rotates the die to face that number.
The result of that is here.
This has been done before, many times, and it’s the obvious place to start.
Make the already rectangular faces square, center them on the container, then rotate them into place with rotateX and rotateY in 90° increments, and finally translate them out by half the die’s size to sit flush on the surface.
.d6 {
width: 100px;
height: 100px;
transform-style: preserve-3d;
}
.d6>* {
position: absolute;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.face-1 { transform: rotateY(0deg) translateZ(50px); }
.face-2 { transform: rotateY(90deg) translateZ(50px); }
.face-3 { transform: rotateY(180deg) translateZ(50px); }
.face-4 { transform: rotateY(-90deg) translateZ(50px); }
.face-5 { transform: rotateX(90deg) translateZ(50px); }
.face-6 { transform: rotateX(-90deg) translateZ(50px); }
It’s mildly annoying how simple the cube is to build, given how much more complicated the other shapes are by comparison. The other shapes require:
- Non-rectangular faces
- Non-trivial face centroids
- More complicated dihedral angles
- Compound rotations
- 3-fold and 5-fold rotational symmetry
- Complicated trigonometry
Each die is a container that holds each face as a child element.
The faces start on the same forward facing plane stacked on top of each other centered by their bounding box.
The central pivot of each die matters! The transform-origin of each face should sit at its centroid on the die’s central axis (not the face’s visual centre), that way the faces rotations fold it predictably rather than swinging it out all about the place.
For the cube, the centroid is just the center of the element, as the element itself is square. But for the other shapes it’s not as easy as transform-origin: 50% 50%.
For each face we:
- Translate to re-center the face to the face’s centroid
- Rotate by a defined rotation for that face to move it into the right orientation
- Translate in the z-axis to push the face outward by the polyhedron’s inradius so it sits flush on the surface of the shape
This common transform pattern that all the dice faces share wasn’t something I designed upfront. It emerged from implementing the first few dice slightly differently and noticing the repeating structure, and refactoring to this form.
The same three-step pattern applies to all six shapes — d4 through d20.
It helps that five of the six dice are Platonic solids — every face and angle are identical — and the geometry falls out cleanly.
The d10 is an exception: a pentagonal trapezohedron, with a non-regular face and two different types of vertex angles.
It required working out which kite I wanted for the face as that would decide if it was going to be a stubby or pointy d10.
Each die needs three things worked out to be able to correctly transform its faces:
- The inradius of the polyhedron
- The dihedral angle between faces
- Some values for the face (aspect ratio and centroid height)
- The rotation for each face, which is usually a combination of 2 or 3 axis rotations
For the most part this was just nutting out the geometry and a lot of scribbling.
I made and corrected many mistakes along the way, but I won’t bore you with the details of that process.
The outcomes for each shape, where s is the edge length, are as follows:
| Shape | Dihedral angle | Inradius | Center-Y |
|---|
| Tetrahedron | acos(1/3) | s√6/12 | 200% / 3 |
| Cube | acos(0) | s/2 | 50% |
| Octahedron | acos(-1/3) | s√6/6 | 200% / 3 |
| Pentagonal trapezohedron | acos(√(2/(φ+2))) | s√(2φ)/4 | 50% |
| Dodecahedron | acos(-√5/5) | sφ√(5φ+10)/10 | 200% / (φ√5) |
| Icosahedron | acos(-√5/3) | sφ²/(2√3) | 200% / 3 |
All of these can be expressed directly in CSS using calc(), acos(), sqrt(), and pow().
That means no pre-computed values anywhere; the stylesheet just holds the geometry.
Despite how mathematically perfect the underlying definitions are, the rendered result is still subject to sub-pixel rounding.
To combat this, each face is scaled up slightly with scale(1.01) to ensure any hairline gaps between faces are “sealed”.
Time to roll some dice!
What started as a simple idea to make a random number generator turned into a fun and challenging set of geometry problems.
I’m really happy I came away from this project with a much better feel for CSS 3D transforms and quite glad to have brushed off my dusty trigonometry skills.
Try out the tool here.