Twisting puzzle cube
Pure CSSA 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.
JS picks a random face and adds full turns; a CSS transition does the tumbling.
Your edited version
+720°) on every roll, so it always tumbles instead of taking a shortcut.transition with an overshoot easing is the roll animation.Math.random() → target rotationaccumulating 360° turnstransition with overshoot easing<div class="table">
<div class="scene">
<div class="cube">
<div>1</div><div>2</div><div>6</div>
<div>5</div><div>3</div><div>4</div>
</div>
</div>
<button type="button">Roll</button>
<output>Click roll</output>
</div>
.table {
display: grid;
justify-items: center;
gap: 50px;
font-family: system-ui;
}
.scene {
perspective: 700px;
}
.cube {
--s: 120px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
transform:
rotateX(-22deg) rotateY(-28deg) /* camera */
rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); /* the roll, from JS */
transition: transform 1.1s cubic-bezier(0.2, 0.9, 0.3, 1.15);
}
.cube > * {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 2px solid #c9cbe0;
border-radius: 16px;
background: radial-gradient(circle at 30% 30%, #fff, #dfe1f0);
color: #1a1d33;
font: 900 3.4rem system-ui;
transform-style: preserve-3d;
}
/* Rounded faces leave a hole where three corners meet. A square plate just behind each face
forms a slightly smaller inner cube that plugs the gaps. */
.cube > *::before {
content: '';
position: absolute;
inset: 8px;
background: #d3d6ea;
transform: translateZ(-8px);
}
/* turn each face outward, then push it half a side from the centre */
.cube > :nth-child(1) { transform: rotateY(0deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(2) { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(5) { transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(6) { transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
button {
padding: 8px 26px;
border: 0;
border-radius: 10px;
background: #ffb547;
font: 800 1rem system-ui;
cursor: pointer;
}
const cube = document.querySelector('.cube');
const out = document.querySelector('output');
// rotation [x, y] that brings each value to the front
// (faces in the markup: front 1, right 2, back 6, left 5, top 3, bottom 4)
const views = { 1: [0, 0], 2: [0, -90], 6: [0, -180], 5: [0, 90], 3: [-90, 0], 4: [90, 0] };
let turns = 0;
document.querySelector('button').addEventListener('click', () => {
const value = 1 + Math.floor(Math.random() * 6);
const [x, y] = views[value];
turns += 2; // two extra full turns per roll
cube.style.setProperty('--rx', x + 360 * turns + 'deg');
cube.style.setProperty('--ry', y + 360 * turns + 'deg');
out.textContent = 'Rolling…';
setTimeout(() => (out.textContent = 'You rolled ' + value), 1100);
});
@use '../mixins' as *;
.d-dice {
display: grid;
grid-template-rows: 1fr auto;
width: 100%;
height: 100%;
padding: 10px 14px 14px; // the control dock: the same place in every demo with controls
&__view {
display: grid;
place-items: center;
perspective: 600px;
}
&__cube {
--s: 92px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
// A fixed camera tilt first, then the roll. JS only ever changes --rx / --ry;
// the long transition is the "tumble".
transform: rotateX(-22deg) rotateY(-28deg) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 1.1s cubic-bezier(0.2, 0.9, 0.3, 1.15);
@include cube-faces(var(--s));
i {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 2px solid #c9cbe0;
border-radius: 14px;
background: radial-gradient(circle at 30% 30%, #fff, #dfe1f0);
color: #1a1d33;
font-size: 44px;
font-style: normal;
font-weight: 900;
@include solid-core(6px, #d3d6ea);
}
}
// the site's control pattern: the status centred on top, the button centred under it
&__bar {
display: grid;
justify-items: center;
gap: 5px;
font-size: 12px;
color: var(--muted);
output {
order: -1;
}
button {
padding: 5px 16px;
border: 0;
border-radius: 8px;
background: var(--warm);
color: #1b1408;
font-weight: 800;
cursor: pointer;
}
output {
color: var(--muted);
}
}
}