Rotating Cube
A real CSS 3D cube turns slowly in space, with your own content on each of its six faces. Drag it to give it a spin and it coasts to a stop before carrying on, or have it turn one face at a time and pause on each.
Plan
Design
Build
Test
Ship
Repeat
How to use it
- Paste the snippet just before
</head>. - Add
data-rotating-cubeto a box with a height, holding six children: front, right, back, left, top and bottom. - Style the faces however you like; the script sizes and places them.
Options
Add any of these attributes to the same element.
data-size | Edge length in px. Default about half the box’s shorter side. |
data-speed | How fast it turns, in degrees per second. Default 24. |
data-tilt | How far the cube leans towards you, in degrees. Default -18. |
data-step | Milliseconds: turn one face at a time, through all six, and pause this long on each. |
data-drag="false" | Don’t let people spin it by dragging. |
With fewer than six children the faces repeat. With reduced motion the cube stands still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Rotating Cube: a 3D cube turns slowly, with your content on each face.
// Drag it to spin it by hand; let go and it carries on turning by itself.
// Usage:
// <div data-rotating-cube>
// <div>One</div><div>Two</div><div>Three</div>
// <div>Four</div><div>Five</div><div>Six</div>
// </div>
// The children become the faces: front, right, back, left, top, bottom.
// With fewer than six they repeat.
// Optional: data-size (edge length in px, default fits the box),
// data-speed (degrees per second, default 24), data-tilt (degrees the cube
// leans towards you, default -18), data-step (ms: turn one face at a time
// and pause on each, instead of turning smoothly), data-drag="false".
var DEFAULTS = { speed: 24, tilt: -18 };
var css =
'.rcb{position:relative;display:grid;place-items:center;perspective:900px;touch-action:pan-y;user-select:none;-webkit-user-select:none}' +
'.rcb-drag{cursor:grab}.rcb-drag:active{cursor:grabbing}' +
'.rcb-cube{position:relative;width:var(--rcb-s);height:var(--rcb-s);transform-style:preserve-3d}' +
'.rcb-face{position:absolute;inset:0;display:grid;place-items:center;box-sizing:border-box;backface-visibility:hidden;-webkit-backface-visibility:hidden}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Where each face sits: [rotateY, rotateX].
var FACES = [[0, 0], [90, 0], [180, 0], [-90, 0], [0, 90], [0, -90]];
// The cube's turn that brings each face to the front, for data-step.
var STOPS = [[0, 0], [-90, 0], [-180, 0], [-270, 0], [-360, -90], [-360, 90]];
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var kids = Array.from(el.children);
if (!kids.length) return;
el.classList.add('rcb');
var cube = document.createElement('div');
cube.className = 'rcb-cube';
for (var i = 0; i < 6; i++) {
var face = i < kids.length ? kids[i] : kids[i % kids.length].cloneNode(true);
if (i >= kids.length) face.setAttribute('aria-hidden', 'true');
face.classList.add('rcb-face');
cube.appendChild(face);
}
el.appendChild(cube);
var faces = Array.from(cube.children);
var tilt = d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var s = { ry: +d.step ? 0 : -30, rx: 0, vy: 0, dragging: false, size: 0 };
function size() {
var px = +d.size || Math.round(Math.min(el.clientWidth, el.clientHeight || el.clientWidth) * 0.52);
if (px === s.size || !px) return;
s.size = px;
cube.style.setProperty('--rcb-s', px + 'px');
faces.forEach(function (f, i) {
f.style.transform = 'rotateY(' + FACES[i][0] + 'deg) rotateX(' + FACES[i][1] + 'deg) translateZ(' + px / 2 + 'px)';
});
}
function draw() {
cube.style.transform = 'rotateX(' + (tilt + s.rx) + 'deg) rotateY(' + s.ry + 'deg)';
}
size();
draw();
if (window.ResizeObserver) new ResizeObserver(size).observe(el);
if (reduced()) return;
var step = +d.step || 0, stop = 0, wait = step, last = 0, base = 0;
// Drag to spin, with a little momentum when you let go.
if (d.drag !== 'false') {
el.classList.add('rcb-drag');
var lastX = 0, lastT = 0;
el.addEventListener('pointerdown', function (e) {
s.dragging = true;
s.vy = 0;
lastX = e.clientX;
lastT = performance.now();
if (s.anim) { s.anim.pause(); s.anim = null; }
el.setPointerCapture(e.pointerId);
});
el.addEventListener('pointermove', function (e) {
if (!s.dragging) return;
var now = performance.now(), dx = e.clientX - lastX;
s.ry += dx * 0.5;
s.vy = (dx * 0.5) / Math.max(8, now - lastT) * 1000;
lastX = e.clientX;
lastT = now;
draw();
});
var end = function () {
if (!s.dragging) return;
s.dragging = false;
if (!step) return;
// Stepping: settle on the nearest side face, then carry on from there.
s.vy = 0;
var k = Math.round(s.ry / 90);
stop = ((-k % 4) + 4) % 4;
base = 90 * k + 90 * stop;
s.anim = anime.animate(s, {
ry: 90 * k, rx: 0, duration: 500, ease: 'out(3)',
onUpdate: draw,
onComplete: function () { s.anim = null; wait = step; }
});
};
el.addEventListener('pointerup', end);
el.addEventListener('pointercancel', end);
}
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) / 1000 : 0.016;
last = now;
if (s.dragging) return;
if (s.vy) {
// Coast after a drag, then settle back to the normal turn.
s.ry += s.vy * dt;
s.vy *= Math.pow(0.04, dt);
if (Math.abs(s.vy) < speed) s.vy = 0;
draw();
return;
}
if (step) {
if (s.anim) return;
wait -= dt * 1000;
if (wait > 0) return;
stop++;
if (stop === STOPS.length) { stop = 0; base -= 360; }
var target = STOPS[stop];
var ry = base + target[0];
s.anim = anime.animate(s, {
ry: ry, rx: target[1], duration: 1100, ease: 'inOut(3)',
onUpdate: draw,
onComplete: function () { s.anim = null; wait = step; }
});
return;
}
s.ry -= speed * dt;
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-rotating-cube]').forEach(function (el) {
if (el._rcb) return;
el._rcb = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-rotating-cube style="height: 320px">
<div class="face">Plan</div>
<div class="face">Design</div>
<div class="face">Build</div>
<div class="face">Test</div>
<div class="face">Ship</div>
<div class="face">Repeat</div>
</div>