Layered text depth
A headline made of stacked copies. The mouse slides each copy a little further, extruding the word into a 3D slab.
CSSJavaScriptIntermediate
<h1 class="word" id="word" style="margin:0">
<span class="front">DEPTH</span>
</h1>
.word{position:relative;font-size:clamp(4rem,22vw,9rem);font-weight:900;letter-spacing:-.04em;line-height:1;color:#f08a5d;cursor:default}
.word span{position:absolute;left:0;top:0;white-space:nowrap;pointer-events:none;will-change:transform}
.word .front{position:relative;display:block;color:#f08a5d}
const word = document.getElementById('word');
const text = word.querySelector('.front').textContent;
const layers = 14;
// back copies, darker the further back they sit
for (let i = layers; i >= 1; i--) {
const s = document.createElement('span');
s.textContent = text;
s.setAttribute('aria-hidden', 'true');
s.style.color = `hsl(17 ${60 - i * 2}% ${52 - i * 2.6}%)`;
s.dataset.i = i;
word.prepend(s);
}
const copies = [...word.querySelectorAll('span[data-i]')];
let tx = 0, ty = 0, cx = 0, cy = 0;
addEventListener('pointermove', e => {
tx = (e.clientX / innerWidth - .5) * 2;
ty = (e.clientY / innerHeight - .5) * 2;
});
(function loop() {
cx += (tx - cx) * 0.1;
cy += (ty - cy) * 0.1;
copies.forEach(s => {
const k = +s.dataset.i * 1.6; // deeper copies shift further
s.style.transform = `translate3d(${cx * k}px, ${cy * k}px, 0)`;
});
requestAnimationFrame(loop);
})();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Layered text depth</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
.word{position:relative;font-size:clamp(4rem,22vw,9rem);font-weight:900;letter-spacing:-.04em;line-height:1;color:#f08a5d;cursor:default}
.word span{position:absolute;left:0;top:0;white-space:nowrap;pointer-events:none;will-change:transform}
.word .front{position:relative;display:block;color:#f08a5d}
</style></head>
<body>
<h1 class="word" id="word" style="margin:0">
<span class="front">DEPTH</span>
</h1>
<script>
const word = document.getElementById('word');
const text = word.querySelector('.front').textContent;
const layers = 14;
// back copies, darker the further back they sit
for (let i = layers; i >= 1; i--) {
const s = document.createElement('span');
s.textContent = text;
s.setAttribute('aria-hidden', 'true');
s.style.color = `hsl(17 ${60 - i * 2}% ${52 - i * 2.6}%)`;
s.dataset.i = i;
word.prepend(s);
}
const copies = [...word.querySelectorAll('span[data-i]')];
let tx = 0, ty = 0, cx = 0, cy = 0;
addEventListener('pointermove', e => {
tx = (e.clientX / innerWidth - .5) * 2;
ty = (e.clientY / innerHeight - .5) * 2;
});
(function loop() {
cx += (tx - cx) * 0.1;
cy += (ty - cy) * 0.1;
copies.forEach(s => {
const k = +s.dataset.i * 1.6; // deeper copies shift further
s.style.transform = `translate3d(${cx * k}px, ${cy * k}px, 0)`;
});
requestAnimationFrame(loop);
})();
</script>
</body></html>
Good for
Hero wordmarks and playful headings on bold brands.
Watch out for
Only the front layer should be real text. Mark the copies aria-hidden. Use a heavy font so the stack reads as one solid shape.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.