Two dice take turns hopping, tumbling a full turn in the air and landing on a new face with a squash. Faces are radial-gradient pips that swap mid-spin, when nobody can see.
Also known as: dice rolling animation, rolling dice css, board game loader.
<div class="ldr-dice" role="status" aria-label="Loading"> <span></span><span></span> </div>
.ldr-dice {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
--die: calc(var(--s) * 0.44);
position: relative;
width: calc(var(--s) * 1.2);
height: var(--s);
}
.ldr-dice::before,
.ldr-dice::after {
content: "";
position: absolute;
bottom: 8%;
left: calc(var(--s) * 0.136);
width: calc(var(--s) * 0.36);
height: calc(var(--s) * 0.06);
border-radius: 50%;
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 25%, transparent);
animation: ldr-dice-shadow 2.4s infinite;
}
.ldr-dice::after {
left: auto;
right: calc(var(--s) * 0.136);
animation-delay: -0.6s;
}
.ldr-dice span {
position: absolute;
z-index: 1;
bottom: 13%;
left: 8%;
width: var(--die);
height: var(--die);
box-sizing: border-box;
border: calc(var(--s) / 18) solid var(--c);
border-radius: calc(var(--s) / 9);
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 12%, transparent);
animation: ldr-dice-hop 2.4s infinite;
}
.ldr-dice span + span {
left: auto;
right: 8%;
animation-delay: -0.6s;
}
.ldr-dice span::before,
.ldr-dice span::after {
content: "";
position: absolute;
inset: 0;
--pip: calc(var(--s) * 0.042);
animation: ldr-dice-face-a 2.4s infinite;
animation-delay: inherit;
}
.ldr-dice span::after {
animation-name: ldr-dice-face-b;
}
.ldr-dice span:first-child::before {
background:
radial-gradient(circle var(--pip) at 24% 24%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 76% 24%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 50% 50%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 24% 76%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 76% 76%, var(--c) 94%, transparent);
}
.ldr-dice span:first-child::after {
background:
radial-gradient(circle var(--pip) at 26% 26%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 74% 74%, var(--c) 94%, transparent);
}
.ldr-dice span + span::before {
background:
radial-gradient(circle var(--pip) at 24% 24%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 50% 50%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 76% 76%, var(--c) 94%, transparent);
}
.ldr-dice span + span::after {
background:
radial-gradient(circle var(--pip) at 27% 22%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 27% 50%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 27% 78%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 73% 22%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 73% 50%, var(--c) 94%, transparent),
radial-gradient(circle var(--pip) at 73% 78%, var(--c) 94%, transparent);
}
@keyframes ldr-dice-hop {
0% { transform: translateY(0) rotate(0) scale(1); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
6% { transform: translateY(calc(var(--die) * 0.075)) rotate(0) scale(1.1, 0.85); animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); }
20% { transform: translateY(calc(var(--s) * -0.36)) rotate(180deg) scale(1); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
34% { transform: translateY(0) rotate(360deg) scale(1); }
38% { transform: translateY(calc(var(--die) * 0.06)) rotate(360deg) scale(1.08, 0.88); }
44%, 50% { transform: translateY(0) rotate(360deg) scale(1); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
56% { transform: translateY(calc(var(--die) * 0.075)) rotate(360deg) scale(1.1, 0.85); animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); }
70% { transform: translateY(calc(var(--s) * -0.36)) rotate(540deg) scale(1); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
84% { transform: translateY(0) rotate(720deg) scale(1); }
88% { transform: translateY(calc(var(--die) * 0.06)) rotate(720deg) scale(1.08, 0.88); }
94%, 100% { transform: translateY(0) rotate(720deg) scale(1); }
}
@keyframes ldr-dice-face-a {
0%, 19.9% { opacity: 1; }
20%, 69.9% { opacity: 0; }
70%, 100% { opacity: 1; }
}
@keyframes ldr-dice-face-b {
0%, 19.9% { opacity: 0; }
20%, 69.9% { opacity: 1; }
70%, 100% { opacity: 0; }
}
@keyframes ldr-dice-shadow {
0%, 6%, 34%, 56%, 84%, 100% { transform: scaleX(1); opacity: 1; }
20%, 70% { transform: scaleX(0.5); opacity: 0.4; }
}
@media (prefers-reduced-motion: reduce) {
.ldr-dice, .ldr-dice *, .ldr-dice::before, .ldr-dice::after, .ldr-dice *::before, .ldr-dice *::after {
animation-duration: 3s;
}
}--ldr-color on the element or a parent.
Currently previewing #3b82f6.--ldr-size. Every dimension in the snippet is
derived from it. Currently previewing 64px.animation shorthand.
Delays on the children are relative, so they keep their rhythm.role="status" and an
aria-label so screen readers announce the wait. The reduced-motion rule
at the end slows the animation for users who asked for less motion.