Letter Slam
Letters drop from huge to normal size one at a time, speeding up as if falling onto the page, and each one lands with a jolt that shakes the whole line.
Hit hard
How to use it
- Paste the snippet just before
</head>. - Add
data-letter-slamto a short heading.
Options
Add any of these attributes to the same element.
data-loop | Slam in, hold, fade away and slam in again. |
data-hold | How long the text stays before fading, when looping. Default 1600 ms. |
data-scale | How big the letters start. Default 3.2. |
data-shake | How hard each landing jolts the line, in px. 0 for none. Default 6. |
data-duration | How long each letter takes to fall. Default 360 ms. |
data-stagger | Gap between letters. Default 120 ms. |
data-delay | Wait before the first letter. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Letter Slam: letters drop from huge to normal size one at a time, as if
// falling towards the page, and each one lands with a jolt that shakes
// the whole line.
// Usage: <h1 data-letter-slam>SLAM</h1>
// Optional: data-loop (letters fade off and slam in again), data-hold (ms
// to stay before fading), data-scale (how big letters start; default 3.2),
// data-shake (how hard each landing jolts the line, in px; default 6, 0
// for none), data-duration (ms for each letter's fall), data-stagger (ms
// between letters), data-delay (ms before starting).
var DEFAULTS = { scale: 3.2, shake: 6, duration: 360, stagger: 120, delay: 0, hold: 1600 };
var css =
'.lsl-inner{display:inline-block}' +
'.lsl-word{display:inline-block;white-space:nowrap}' +
'.lsl-char{display:inline-block;will-change:transform,opacity}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// The letters go inside one inner span, so the jolt can shake them all
// without touching the element's own transform.
function split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var inner = document.createElement('span');
inner.className = 'lsl-inner';
inner.setAttribute('aria-hidden', 'true');
var chars = [];
text.split(' ').forEach(function (word, i) {
if (i) inner.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'lsl-word';
Array.from(word).forEach(function (c) {
var s = document.createElement('span');
s.className = 'lsl-char';
s.textContent = c;
w.appendChild(s);
chars.push(s);
});
inner.appendChild(w);
});
el.appendChild(inner);
return { inner: inner, chars: chars };
}
// A short, decaying shudder. Each landing starts a fresh one, taking over
// from wherever the last one had got to.
function jolt(inner, shake, side) {
anime.animate(inner, {
y: [
{ to: shake, duration: 45, ease: 'out(2)' },
{ to: -shake * 0.45, duration: 80, ease: 'inOut(2)' },
{ to: shake * 0.2, duration: 80, ease: 'inOut(2)' },
{ to: 0, duration: 120, ease: 'out(2)' }
],
rotate: [
{ to: side * shake * 0.12, duration: 45, ease: 'out(2)' },
{ to: 0, duration: 260, ease: 'out(2)' }
]
});
}
function play(el) {
var st = el._lsl;
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var duration = +d.duration || DEFAULTS.duration;
var stagger = +d.stagger || DEFAULTS.stagger;
var scale = +d.scale || DEFAULTS.scale;
var shake = d.shake !== undefined ? +d.shake : DEFAULTS.shake;
var tl = anime.createTimeline({ loop: loop, loopDelay: 400, delay: +d.delay || DEFAULTS.delay });
st.chars.forEach(function (c, i) {
var at = i * stagger;
// Speeding up as it falls, so it hits rather than glides.
tl.add(c, {
scale: { from: scale, to: 1, ease: 'in(3)' },
opacity: { from: 0, to: 1, duration: duration * 0.6, ease: 'out(2)' },
duration: duration
}, at);
if (shake) tl.call(function () { jolt(st.inner, shake, i % 2 ? 1 : -1); }, at + duration);
});
if (loop) {
tl.add(st.chars, {
opacity: 0,
scale: 0.85,
duration: 500,
ease: 'in(2)',
delay: anime.stagger(30)
}, '+=' + (+d.hold || DEFAULTS.hold));
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-letter-slam]').forEach(function (el) {
if (el._lsl) return;
el._lsl = split(el);
if (reduced()) return;
anime.utils.set(el._lsl.chars, { opacity: 0 });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-letter-slam>Hit hard</h1>
<!-- a gentler landing: -->
<h1 data-letter-slam data-scale="2" data-shake="3">Hit hard</h1>