Logo 3D Spin
A logo flips round in 3D a couple of times, rising as it spins and slowing down, then lands and settles with a little wobble. Its back shows the logo too, the right way round, and it can have a solid edge like a coin, with a shadow that follows the turn.
How to use it
- Paste the snippet just before
</head>. - Add
data-logo-3d-spinto your logo: an<img>, an inline<svg>or a styled badge.
Options
Add any of these attributes to the same element.
data-turns | Full turns per spin. Default 2. |
data-duration | Time for a spin. Default 2200 ms. |
data-depth | Thickness in pixels, for a coin edge. It takes the element’s background colour, a little darker. Default 0. |
data-loop | Spin again every few seconds, alternating direction. |
data-interval | Pause between spins, when looping. Default 2600 ms. |
data-hover | Spin once more when the pointer arrives. |
data-shadow="false" | No shadow underneath. |
It starts when scrolled into view. The back is a copy of the logo, hidden from screen readers. With reduced motion the logo stays still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Logo 3D Spin: a logo flips round in 3D a couple of times, rising as it
// spins and slowing down, then lands and settles with a little wobble. Its
// back shows the logo too (the right way round), and with data-depth it
// gets a solid edge, like a coin. A shadow underneath follows the turn.
// Starts when scrolled into view.
// Usage: <img data-logo-3d-spin src="logo.svg" alt="Animacious">
// Works on any element: an <img>, an <svg> or a styled <div> badge.
// Optional: data-turns (full turns, default 2), data-duration (ms, default
// 2200), data-depth (thickness in px for a coin edge, default 0; the edge
// takes the element's background colour, a little darker), data-loop (spin
// again every few seconds, alternating direction), data-interval (ms between
// spins when looping, default 2600), data-hover (spin once more on hover),
// data-shadow="false" (no shadow).
var DEFAULTS = { turns: 2, duration: 2200, interval: 2600 };
var css =
'.lsp{position:relative;display:inline-grid;justify-items:center;perspective:700px;vertical-align:middle}' +
'.lsp-coin{position:relative;display:grid;transform-style:preserve-3d}' +
'.lsp-coin>*{grid-area:1/1}' +
'.lsp-face{backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.lsp-edge{pointer-events:none}' +
'.lsp-shadow{width:70%;height:12px;margin-top:10px;border-radius:50%;background:radial-gradient(closest-side,rgba(20,10,40,.28),rgba(20,10,40,0))}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var depth = Math.max(0, +d.depth || 0);
var wrap = document.createElement('span');
wrap.className = 'lsp';
var coin = document.createElement('span');
coin.className = 'lsp-coin';
el.parentNode.insertBefore(wrap, el);
wrap.appendChild(coin);
// The back: a copy of the logo, turned round so it reads the right way.
var back = el.cloneNode(true);
back.removeAttribute('data-logo-3d-spin');
back.removeAttribute('id');
back.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
back.setAttribute('aria-hidden', 'true');
if (back.tagName === 'IMG') back.alt = '';
el.classList.add('lsp-face');
back.classList.add('lsp-face');
var half = depth / 2;
el.style.transform = 'translateZ(' + (half + 0.2) + 'px)';
back.style.transform = 'rotateY(180deg) translateZ(' + (half + 0.2) + 'px)';
// The edge: thin layers stacked through the thickness, a shade darker.
if (depth) {
var cs = getComputedStyle(el);
var bg = cs.backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
var n = Math.ceil(depth / 1.5);
for (var i = 0; i <= n; i++) {
var layer = document.createElement('span');
layer.className = 'lsp-edge';
layer.setAttribute('aria-hidden', 'true');
layer.style.cssText = 'border-radius:' + cs.borderRadius + ';background:' + bg + ';filter:brightness(.68);' +
'transform:translateZ(' + (-half + (depth * i) / n) + 'px)';
coin.appendChild(layer);
}
}
}
coin.appendChild(back);
coin.appendChild(el);
var shadow = null;
if (d.shadow !== 'false') {
shadow = document.createElement('span');
shadow.className = 'lsp-shadow';
shadow.setAttribute('aria-hidden', 'true');
wrap.appendChild(shadow);
}
return { el: el, wrap: wrap, coin: coin, shadow: shadow, state: { a: 0 }, dir: 1, busy: false };
}
// The shadow narrows as the logo turns side-on, and shrinks as it rises.
function render(s, lift) {
if (!s.shadow) return;
var side = Math.abs(Math.cos((s.state.a * Math.PI) / 180));
var h = s.coin.offsetHeight || 1;
var up = Math.min(1, -lift / (h * 0.25));
s.shadow.style.transform = 'scaleX(' + (0.35 + 0.65 * side) * (1 - up * 0.3) + ')';
s.shadow.style.opacity = 1 - up * 0.45;
}
function spin(s, turns, duration) {
if (s.busy) return;
s.busy = true;
var dir = s.dir;
s.dir = -s.dir;
var h = s.coin.offsetHeight;
var lift = { y: 0 };
var end = s.state.a + dir * 360 * turns;
// Fast to begin with, then an overshoot that springs back into place.
anime.animate(s.state, {
a: [{ to: end + dir * 14, duration: duration * 0.72, ease: 'out(3)' }, { to: end, duration: duration * 0.5, ease: 'outElastic(1, .4)' }],
onUpdate: function () {
s.coin.style.transform = 'translateY(' + lift.y + 'px) rotateY(' + s.state.a + 'deg)';
render(s, lift.y);
},
onComplete: function () {
s.state.a = 0;
s.coin.style.transform = '';
s.busy = false;
}
});
anime.animate(lift, {
y: [{ to: -h * 0.22, duration: duration * 0.35, ease: 'out(2)' }, { to: 0, duration: duration * 0.4, ease: 'outBounce' }]
});
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-logo-3d-spin]').forEach(function (el) {
if (el._lsp || reduced) return;
var s = (el._lsp = build(el));
var d = el.dataset;
var turns = +d.turns || DEFAULTS.turns;
var duration = +d.duration || DEFAULTS.duration;
var loop = d.loop !== undefined && d.loop !== 'false';
var timer = null, seen = false;
var next = function () {
timer = setTimeout(function () { spin(s, turns, duration); next(); }, duration * 1.2 + (+d.interval || DEFAULTS.interval));
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (!entries[0].isIntersecting) return;
if (!seen) { seen = true; spin(s, turns, duration); }
if (loop) next();
}, { threshold: 0.4 }).observe(s.wrap);
if (d.hover !== undefined && d.hover !== 'false') {
s.wrap.addEventListener('pointerenter', function () { spin(s, 1, duration * 0.7); });
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><img data-logo-3d-spin src="/logo.svg" alt="Your brand" width="96">
<!-- a coin with an edge: -->
<div class="badge" data-logo-3d-spin data-depth="10" data-loop>…</div>