Extruded Text Turn
Chunky 3D letters, built from a stack of copies set back in space, sway slowly from side to side and nod up and down, so you see their deep sides catch the light.
SOLID
How to use it
- Paste the snippet just before
</head>. - Add
data-extruded-text-turnto a short, bold heading. The front takes the text’s colour; pick a darkerdata-side-colorfor the sides.
Options
Add any of these attributes to the same element.
data-depth | How deep the letters are. Default 0.35 em. |
data-side-color | Colour of the sides, darkening towards the back. Default #4c1d95. |
data-angle | How far it turns each way. Default 30°. |
data-speed | Time for one swing. Default 3600 ms. |
data-layers | Copies used to build the sides; more is smoother. Default 24. |
Short words in a heavy weight look best. With reduced motion turned on the text rests at a slight angle, still showing its depth.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Extruded Text Turn: deep 3D letters, built from a stack of copies set
// back in space, sway slowly from side to side so you can see their sides.
// Usage: <h1 data-extruded-text-turn>Deep type</h1>
// Optional: data-depth (how deep the letters are, in em; default 0.35),
// data-layers (copies used to build the sides; more is smoother; default 24),
// data-side-color (colour of the sides; default #4c1d95), data-angle (how
// far it turns each way, degrees; default 30), data-speed (ms for one swing;
// default 3600).
var DEFAULTS = { depth: 0.35, layers: 24, sideColor: '#4c1d95', angle: 30, speed: 3600 };
var css =
'.ext-scene{display:inline-block;perspective:7em;perspective-origin:50% 50%}' +
'.ext-obj{position:relative;display:inline-block;transform-style:preserve-3d;will-change:transform}' +
'.ext-face{position:relative;display:block}' +
'.ext-side{position:absolute;left:0;top:0;width:100%;height:100%;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var depth = d.depth !== undefined ? +d.depth : DEFAULTS.depth;
var layers = Math.max(2, Math.min(60, +d.layers || DEFAULTS.layers));
var side = d.sideColor || DEFAULTS.sideColor;
var angle = d.angle !== undefined ? +d.angle : DEFAULTS.angle;
var speed = +d.speed || DEFAULTS.speed;
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var scene = document.createElement('span');
scene.className = 'ext-scene';
scene.setAttribute('aria-hidden', 'true');
var obj = document.createElement('span');
obj.className = 'ext-obj';
// The copies, from the back forwards: darkest at the back, so the sides
// look lit from the front.
for (var i = layers; i >= 1; i--) {
var s = document.createElement('span');
s.className = 'ext-side';
s.textContent = text;
var dark = Math.round(55 * i / layers);
s.style.color = 'color-mix(in srgb, ' + side + ' ' + (100 - dark) + '%, #000)';
s.style.transform = 'translateZ(' + (-depth * i / layers).toFixed(4) + 'em)';
obj.appendChild(s);
}
var face = document.createElement('span');
face.className = 'ext-face';
face.textContent = text;
obj.appendChild(face);
scene.appendChild(obj);
el.appendChild(scene);
if (reduced()) {
anime.utils.set(obj, { rotateY: -angle * 0.6, rotateX: 10 });
return;
}
anime.utils.set(obj, { rotateY: -angle, rotateX: 8 });
var turn = anime.animate(obj, {
rotateY: [-angle, angle],
duration: speed,
ease: 'inOutSine',
loop: true,
alternate: true,
autoplay: false
});
// A slower nod up and down, so the top and bottom show too.
var nod = anime.animate(obj, {
rotateX: [12, -6],
duration: speed * 1.7,
ease: 'inOutSine',
loop: true,
alternate: true,
autoplay: false
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { turn.play(); nod.play(); }
else { turn.pause(); nod.pause(); }
}).observe(el);
}
function init() {
document.querySelectorAll('[data-extruded-text-turn]').forEach(function (el) {
if (el._ext) return;
el._ext = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-extruded-text-turn style="color: #a855f7">SOLID</h1>
<!-- deeper, in teal: -->
<h1 data-extruded-text-turn data-depth="0.5" data-side-color="#0f766e" style="color: #2dd4bf">DEEP</h1>