How to Build 3D Dice That Roll with JavaScript and CSS

How to Build 3D Dice That Roll with JavaScript and CSS
Try it Yourself 🕹

Let’s create an intuitive, nicely designed, and lightweight app that can be used for any situation that requires random number generation.

It will teach us that building applications without any library or framework are possible. We will use only JavaScript, CSS, and HTML. Click on the “Try it Yourself” button to see how it works.

So, let’s begin!

Part I: Initial Setup 🛠

As we already mentioned, we will not use any library or framework, which means we will not need a JavaScript runtime environment such as Node.js, etc.

Let’s create a folder for the project, then move inside the folder, and create the files we will need. To achieve all of this, execute the following commands one by one:

# crate a folder for the project
mkdir 3d-dice-roll-javascript-css-html

# move inside the project
cd 3d-dice-roll-javascript-css-html

# create the files we will need
touch index.html app.js style.css

📝 The mkdir command in Linux/Unix allows users to create or make new directories. mkdir stands for “make directory”. The touch command is used to create a file without any content. The file created using the touch command is empty. This command can be used when the user doesn’t have data to store at the time of file creation.

Part II: Building The App 🚀

After successfully creating the files, let’s move on to building our application. First, let’s open index.html and add the following code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Dice That Roll</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
	<!--Our app HTML will be here-->
	<script src="app.js"></script>
  </body>
</html>

We will now add the structure for two dice, with CSS classes already attached to the HTML elements.

<div class="dice">
    <ol class="die-list even-roll" data-roll="1" id="die-1">
        <li class="die-item" data-side="1">
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="2">
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="3">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="4">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="5">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="6">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
    </ol>
    <ol class="die-list odd-roll" data-roll="1" id="die-2">
        <li class="die-item" data-side="1">
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="2">
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="3">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="4">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="5">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
        <li class="die-item" data-side="6">
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
            <span class="dot"></span>
        </li>
    </ol>
</div>

Finally, we also need to add a button to start rolling the dice on the user’s click.

<button id="roll-btn">Roll Dice</button>

📝 The code snippet for the button should be after the code snippet for the dice, but before <script src="app.js"></script>.

If we open the index.html file in the browser we will see two lists from 1 to 6 and a button. Now let’s update our main styling file - style.css - with some CSS code.

* {
    margin: 0;
    padding: 0;
    vertical-align: baseline;
}
html {
    font-family: system-ui, sans-serif;
}
body {
    background: linear-gradient(#545454, #454545, #676767);
    display: grid;
    grid-template-columns: 1fr;
    height: 100vh;
    overflow: hidden;
    width: 100%;
}
.dice {
    align-items: center;
    display: grid;
    grid-gap: 2rem;
    grid-template-columns: auto;
    grid-template-rows: repeat(auto-fit, minmax(8rem, 1fr));
    justify-items: center;
    padding: 2rem;
    perspective: 600px;
}
@media (min-width: 600px) {
    .dice {
        perspective: 1300px;
        grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
        grid-template-rows: auto;
    }
}
.die-list {
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    height: 6rem;
    list-style-type: none;
    transform-style: preserve-3d;
    width: 6rem;
}
.die-item {
    background-color: #fefefe;
    box-shadow: inset -0.35rem 0.35rem 0.75rem rgba(0, 0, 0, 0.3),
    inset 0.5rem -0.25rem 0.5rem rgba(0, 0, 0, 0.15);
    display: grid;
    grid-column: 1;
    grid-row: 1;
    grid-template-areas:
    'one two three'
    'four five six'
    'seven eight nine';
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
    height: 100%;
    padding: 1rem;
    width: 100%;
}
button {
    transition: all 0.3s;
    position: relative;
    top: 0;
    cursor: pointer;
    user-select: none;
    align-self: center;
    background-color: #fefefe;
    box-shadow: 0 7px 0px #cc002d;
    border: none;
    border-radius: 7px;
    color: #333;
    font-size: 1.75rem;
    font-weight: 700;
    justify-self: center;
    padding: 0.5rem 1rem;
}
button:active {
    top: 5px;
    box-shadow: 0 2px 0px #cc002d;
    transition: all 0.3s;
}

This is the initial styling and if you save the file and open (or refresh) index.html in the browser you should see two white squares and a nicely designed button that can be clicked already.

To be able to follow the CSS part of this coding challenge, you should have a basic understanding of CSS Grid Layout (aka “Grid” or “CSS Grid”).

It is a two-dimensional grid-based layout system that completely changes the way we design user interfaces compared to any web layout system of the past.

We also use transform-style - CSS property that sets whether children of an element are positioned in the 3D space or flattened in the element’s plane.

And the perspective CSS property determines the distance between the z=0 plane and the user in order to give a 3D-positioned element some perspective.

Let’s make a cube from the square …

[data-side='1'] {
  transform: rotate3d(0, 0, 0, 0.25turn) translateZ(4rem);
}
[data-side='2'] {
  transform: rotate3d(-1, 0, 0, 0.25turn) translateZ(4rem);
}
[data-side='3'] {
  transform: rotate3d(0, 1, 0, 0.25turn) translateZ(4rem);
}
[data-side='4'] {
  transform: rotate3d(0, -1, 0, 0.25turn) translateZ(4rem);
}
[data-side='5'] {
  transform: rotate3d(1, 0, 0, 0.25turn) translateZ(4rem);
}
[data-side='6'] {
  transform: rotate3d(1, 0, 0, 0.5turn) translateZ(4rem);
}

… and add some dots on it.

.dot {
    align-self: center;
    background-color: #676767;
    border-radius: 50%;
    box-shadow: inset -0.15rem 0.15rem 0.25rem rgba(0, 0, 0, 0.5);
    display: block;
    height: 1.25rem;
    justify-self: center;
    width: 1.25rem;
}
[data-side='1'] .dot:nth-of-type(1) {
  grid-area: five;
}
[data-side='2'] .dot:nth-of-type(1) {
  grid-area: one;
}
[data-side='2'] .dot:nth-of-type(2) {
  grid-area: nine;
}
[data-side='3'] .dot:nth-of-type(1) {
  grid-area: one;
}
[data-side='3'] .dot:nth-of-type(2) {
  grid-area: five;
}
[data-side='3'] .dot:nth-of-type(3) {
  grid-area: nine;
}
[data-side='4'] .dot:nth-of-type(1) {
  grid-area: one;
}
[data-side='4'] .dot:nth-of-type(2) {
  grid-area: three;
}
[data-side='4'] .dot:nth-of-type(3) {
  grid-area: seven;
}
[data-side='4'] .dot:nth-of-type(4) {
  grid-area: nine;
}
[data-side='5'] .dot:nth-of-type(1) {
  grid-area: one;
}
[data-side='5'] .dot:nth-of-type(2) {
  grid-area: three;
}
[data-side='5'] .dot:nth-of-type(3) {
  grid-area: five;
}
[data-side='5'] .dot:nth-of-type(4) {
  grid-area: seven;
}
[data-side='5'] .dot:nth-of-type(5) {
  grid-area: nine;
}
[data-side='6'] .dot:nth-of-type(1) {
  grid-area: one;
}
[data-side='6'] .dot:nth-of-type(2) {
  grid-area: three;
}
[data-side='6'] .dot:nth-of-type(3) {
  grid-area: four;
}
[data-side='6'] .dot:nth-of-type(4) {
  grid-area: six;
}
[data-side='6'] .dot:nth-of-type(5) {
  grid-area: seven;
}
[data-side='6'] .dot:nth-of-type(6) {
  grid-area: nine;
}

Finally, with transition and transform we will add rolling animation to the cubes.

.even-roll {
  transition: transform 2s ease-out;
}
.odd-roll {
  transition: transform 2s ease-out;
}
.even-roll[data-roll='1'] {
  transform: rotateX(1turn) rotateY(2turn) rotateZ(1turn);
}
.even-roll[data-roll='2'] {
  transform: rotateX(1.25turn) rotateY(2turn) rotateZ(1turn);
}
.even-roll[data-roll='3'] {
  transform: rotateX(1turn) rotateY(1.75turn) rotateZ(1turn);
}
.even-roll[data-roll='4'] {
  transform: rotateX(1turn) rotateY(2.25turn) rotateZ(1turn);
}
.even-roll[data-roll='5'] {
  transform: rotateX(0.75turn) rotateY(2turn) rotateZ(1turn);
}
.even-roll[data-roll='6'] {
  transform: rotateX(1turn) rotateY(2.5turn) rotateZ(1turn);
}
.odd-roll[data-roll='1'] {
  transform: rotateX(-1turn) rotateY(-2turn) rotateZ(-1turn);
}
.odd-roll[data-roll='2'] {
  transform: rotateX(-0.75turn) rotateY(-2turn) rotateZ(-1turn);
}
.odd-roll[data-roll='3'] {
  transform: rotateX(-1turn) rotateY(-2.25turn) rotateZ(-1turn);
}
.odd-roll[data-roll='4'] {
  transform: rotateX(-1turn) rotateY(-1.75turn) rotateZ(-1turn);
}
.odd-roll[data-roll='5'] {
  transform: rotateX(-1.25turn) rotateY(-2turn) rotateZ(-1turn);
}
.odd-roll[data-roll='6'] {
  transform: rotateX(-1turn) rotateY(-2.5turn) rotateZ(-1turn);
}

Of course, transition and transform is not enough to achieve animation. We will need also JavaScript code inside app.js, that will generate random number for each dice and toggle the css classes for roll. For that, we select all the dice with querySelectorAll, iterate through them and set the random number for each die. All this we wrap in a function and add it as a listener on the button click event.

function rollDice() {
  const dice = [...document.querySelectorAll('.die-list')];
  dice.forEach((die) => {
    toggleClasses(die);
    die.dataset.roll = getRandomNumber(1, 6);
  });
}

function toggleClasses(die) {
  die.classList.toggle('odd-roll');
  die.classList.toggle('even-roll');
}

function getRandomNumber(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

document.getElementById('roll-btn').addEventListener('click', rollDice); 

If your index.html file is already running in the browser, refresh and check the final result. Inside the application, on button click, you should be able to see the animated 3D rolling dice.

If everything was right, we can say that our application works flawlessly! 🎉