Dice roll
CSS + JSJS picks a random face and adds full turns; a CSS transition does the tumbling.
A 3×3×3 cube built as three layers that take turns twisting a quarter turn. Each layer is one flat box with the stickers painted on by gradients.
Your edited version
-11s and -10s shift layers 2 and 3 one and two seconds later, so they take turns.backface-visibility: hidden: the two touching faces point in opposite directions, so only one is ever drawn and they never flicker.one box per layer, not 27 cubeletsgradient sticker gridshared keyframes + negative delays take turnsbackface-visibility on inner faces<div class="scene">
<div class="rubik">
<div class="layer"><i></i><i></i><i></i><i></i><b class="top"></b><b></b></div>
<div class="layer"><i></i><i></i><i></i><i></i><b></b><b></b></div>
<div class="layer"><i></i><i></i><i></i><i></i><b></b><b class="bottom"></b></div>
</div>
</div>
.scene {
perspective: 800px;
}
.rubik {
position: relative;
width: 120px;
height: 120px;
transform-style: preserve-3d;
transform: rotateX(-28deg) rotateY(-38deg);
}
/* one layer = one flat box, 120 x 40 x 120 */
.layer {
position: absolute;
left: 0;
width: 120px;
height: 40px;
transform-style: preserve-3d;
animation: twist 12s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.layer:nth-child(1) { top: 0; }
.layer:nth-child(2) { top: 40px; animation-delay: -11s; } /* one second later */
.layer:nth-child(3) { top: 80px; animation-delay: -10s; } /* two seconds later */
/* stickers: black lines on the tile edges, 3 across and --row down */
.layer > * {
position: absolute;
left: 0;
background-color: var(--c, #10121f);
background-image:
linear-gradient(90deg, #10121f 3px, transparent 3px calc(100% - 3px), #10121f calc(100% - 3px)),
linear-gradient(#10121f 3px, transparent 3px calc(100% - 3px), #10121f calc(100% - 3px));
background-size: 33.334% 100%, 100% var(--row, 100%);
backface-visibility: hidden;
}
/* four sides: strips of three stickers */
.layer i { top: 0; width: 120px; height: 40px; }
.layer i:nth-child(1) { --c: #8b6cff; transform: translateZ(60px); }
.layer i:nth-child(2) { --c: #2ee6d6; transform: rotateY(90deg) translateZ(60px); }
.layer i:nth-child(3) { --c: #ff4d9d; transform: rotateY(180deg) translateZ(60px); }
.layer i:nth-child(4) { --c: #ffb547; transform: rotateY(-90deg) translateZ(60px); }
/* top and bottom of a layer: black inside the cube, stickers only on the outside */
.layer b {
--row: 33.334%;
top: -40px;
width: 120px;
height: 120px;
transform: rotateX(90deg) translateZ(20px);
}
.layer b + b {
transform: rotateX(-90deg) translateZ(20px);
}
.layer .top { --c: #f2f3ff; }
.layer .bottom { --c: #4d8dff; }
/* a quarter turn in the first 6% of each quarter, then hold */
@keyframes twist {
0% { transform: rotateY(0deg); }
6%, 25% { transform: rotateY(90deg); }
31%, 50% { transform: rotateY(180deg); }
56%, 75% { transform: rotateY(270deg); }
81%, 100% { transform: rotateY(360deg); }
}
@use 'sass:math';
$s: 90px; // whole cube
$l: math.div($s, 3); // one layer
$k: #10121f; // the black plastic between stickers
$line: 2.5px;
.d-rubik {
position: relative;
width: $s;
height: $s;
transform-style: preserve-3d;
transform: rotateX(-28deg) rotateY(-38deg);
// 27 cubelets would be 162 faces. A layer only ever turns as one piece, so each layer is ONE
// flat box (6 faces) and the 3x3 stickers are painted on with gradients.
&__layer {
position: absolute;
left: 0;
width: $s;
height: $l;
transform-style: preserve-3d;
// One shared animation. Every layer runs the same 12-slot timeline (a quarter turn at the
// start of every third slot); negative delays put layer 2 one slot behind and layer 3 two
// slots behind, so they take turns. After 12 slots each layer has done 360deg: a seamless loop.
animation: d-rubik-twist 12s cubic-bezier(0.65, 0, 0.35, 1) infinite;
&:nth-child(1) {
top: 0;
}
&:nth-child(2) {
top: $l;
animation-delay: -11s;
}
&:nth-child(3) {
top: $l * 2;
animation-delay: -10s;
}
}
i,
b {
position: absolute;
left: 0;
background-color: var(--c, #{$k});
// two line patterns: vertical gaps repeat three times across, horizontal gaps repeat --rows times
background-image:
linear-gradient(90deg, $k $line, transparent $line calc(100% - #{$line}), $k calc(100% - #{$line})),
linear-gradient($k $line, transparent $line calc(100% - #{$line}), $k calc(100% - #{$line}));
background-size:
33.334% 100%,
100% var(--row, 100%);
backface-visibility: hidden;
}
// the four sides of a layer: a strip of three stickers
i {
top: 0;
width: $s;
height: $l;
&:nth-child(1) {
--c: var(--accent);
transform: translateZ($s * 0.5);
}
&:nth-child(2) {
--c: var(--accent-2);
transform: rotateY(90deg) translateZ($s * 0.5);
}
&:nth-child(3) {
--c: var(--hot);
transform: rotateY(180deg) translateZ($s * 0.5);
}
&:nth-child(4) {
--c: var(--warm);
transform: rotateY(-90deg) translateZ($s * 0.5);
}
}
// top and bottom of a layer: full squares, centred on the layer, pushed out by half its height.
// Inside the cube they stay plain black; only the outer two carry stickers (class --cap).
b {
--row: 33.334%;
top: ($l - $s) * 0.5;
width: $s;
height: $s;
transform: rotateX(90deg) translateZ($l * 0.5);
& + b {
transform: rotateX(-90deg) translateZ($l * 0.5);
}
}
&__cap {
--c: #f2f3ff; // top of the cube
}
&__layer:nth-child(3) &__cap {
--c: #4d8dff; // bottom of the cube
}
}
@keyframes d-rubik-twist {
0% {
transform: rotateY(0deg);
}
6%,
25% {
transform: rotateY(90deg);
}
31%,
50% {
transform: rotateY(180deg);
}
56%,
75% {
transform: rotateY(270deg);
}
81%,
100% {
transform: rotateY(360deg);
}
}