3D Word Cube
One word in your sentence sits on a cube that rolls forward to show the next word. Faces dim as they turn away, like a real object catching the light.
Make it fast
How to use it
- Paste the snippet just before
</head>. - Wrap the changing word in a
<span>and list every word, separated by commas, indata-word-cube.
Options
Add any of these attributes to the same element.
data-hold | How long each word stays. Default 1800 ms. |
data-duration | How long each roll takes. Default 800 ms. |
data-direction | up (default): the next word rolls up from below. down: it drops in from above. |
The cube is as wide as the longest word and sits on the sentence’s baseline, so nothing around it moves. It takes its font, size and colour from your text.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// 3D Word Cube: one word in a sentence sits on a cube that rolls forward to
// show the next word. Faces dim as they turn away, like a lit object.
// Usage: <h1>Make it <span data-word-cube="fast, simple, yours">fast</span></h1>
// Optional: data-hold (ms per word), data-duration (ms per roll),
// data-direction ("up" or "down").
var DEFAULTS = { hold: 1800, duration: 800, ease: 'outBack(1.3)' };
var css =
'.wcx{display:inline-grid;position:relative;vertical-align:baseline;text-align:left;perspective:8em}' +
'.wcx-size{grid-area:1/1;visibility:hidden;white-space:nowrap}' +
'.wcx-cube{position:absolute;inset:0;transform-style:preserve-3d}' +
'.wcx-face{position:absolute;inset:0;white-space:nowrap;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.wcx-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function span(cls, parent, text) {
var s = document.createElement('span');
s.className = cls;
if (text !== undefined) s.textContent = text;
parent.appendChild(s);
return s;
}
function build(el) {
var words = (el.getAttribute('data-word-cube') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
el.classList.add('wcx');
el.textContent = '';
span('wcx-sr', el, words.join(', '));
// Hidden copies of every word: the box takes the widest one and sits on the
// sentence's baseline, so nothing around it shifts.
words.forEach(function (w) { span('wcx-size', el, w).setAttribute('aria-hidden', 'true'); });
var cube = span('wcx-cube', el);
cube.setAttribute('aria-hidden', 'true');
// "up": the next word rolls up from below; "down": it drops in from above.
var dir = el.dataset.direction === 'down' ? -1 : 1;
var faces = [];
for (var i = 0; i < 4; i++) faces.push(span('wcx-face', cube, i ? '' : words[0]));
var state = { el: el, words: words, faces: faces, cube: cube, dir: dir, step: 0, angle: 0, depth: 0 };
var measure = function () { state.depth = el.offsetHeight / 2; render(state); };
measure();
if (window.ResizeObserver) new ResizeObserver(measure).observe(el);
return state;
}
function render(s) {
// Rotate about the cube's centre, half a line-height behind the text.
s.cube.style.transform =
'translateZ(' + -s.depth + 'px) rotateX(' + s.angle + 'deg)';
s.faces.forEach(function (f, i) {
var faceAngle = -s.dir * i * 90;
f.style.transform = 'rotateX(' + faceAngle + 'deg) translateZ(' + s.depth + 'px)';
var c = Math.cos(((faceAngle + s.angle) * Math.PI) / 180);
f.style.opacity = c > 0.02 ? (0.2 + 0.8 * c * c).toFixed(3) : 0;
});
}
function start(s) {
if (s.words.length < 2) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var d = s.el.dataset;
(function next() {
s.step++;
// Write the next word onto the face about to come into view.
s.faces[s.step % 4].textContent = s.words[s.step % s.words.length];
anime.animate(s, {
angle: s.dir * s.step * 90,
duration: +d.duration || DEFAULTS.duration,
delay: +d.hold || DEFAULTS.hold,
ease: DEFAULTS.ease,
onUpdate: function () { render(s); },
onComplete: next
});
})();
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
start(e.target._wcx);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-word-cube]').forEach(function (el) {
if (el._wcx) return;
el._wcx = build(el);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Make it
<span data-word-cube="fast, simple, yours">fast</span>
</h1>