Fade-up Stagger
The classic. Words fade in and lift gently into place one after another, with no mask and no fuss. It suits almost any heading.
Simple, smooth and timeless
How to use it
- Paste the snippet just before
</head>. - Add
data-fade-up-staggerto any heading or text element.
Options
Add any of these attributes to the same element.
data-loop | Fade in, hold, fade up and away, and in again. |
data-hold | How long the text stays before fading away, when looping. Default 1800 ms. |
data-distance | How far the words lift, in em. Default 0.6. |
data-duration | How long each word takes to arrive. Default 900 ms. |
data-stagger | Gap between words. Default 80 ms. |
data-delay | Wait before the first word. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Fade-up Stagger: the classic. Words fade in and lift gently into place,
// one after another, with no mask and no fuss.
// Usage: <h1 data-fade-up-stagger>Simple, smooth and timeless</h1>
// Optional: data-loop (words fade up and away, then in again), data-hold
// (ms to stay before fading away), data-distance (how far words lift, in
// em; default 0.6), data-duration (ms per word), data-stagger (ms between
// words), data-delay (ms before starting).
var DEFAULTS = { distance: 0.6, duration: 900, stagger: 80, delay: 0, hold: 1800 };
var css = '.fus-word{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;
}
function split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
return text.split(' ').map(function (word, i) {
if (i) el.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'fus-word';
w.setAttribute('aria-hidden', 'true');
w.textContent = word;
el.appendChild(w);
return w;
});
}
function play(el, words) {
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 distance = +d.distance || DEFAULTS.distance;
var tl = anime.createTimeline({ loop: loop, loopDelay: 300, delay: +d.delay || DEFAULTS.delay });
tl.add(words, {
opacity: [0, 1],
y: [distance + 'em', '0em'],
duration: duration,
ease: 'out(3)',
delay: anime.stagger(stagger)
});
if (loop) {
// Keep drifting upwards as they fade, so the motion has one direction.
tl.add(words, {
opacity: 0,
y: -distance / 2 + 'em',
duration: duration * 0.6,
ease: 'in(2)',
delay: anime.stagger(stagger * 0.5)
}, '+=' + (+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, e.target._fusWords);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-fade-up-stagger]').forEach(function (el) {
if (el._fusWords) return;
var words = split(el);
el._fusWords = words;
if (reduced()) return;
anime.utils.set(words, { opacity: 0 });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-fade-up-stagger>Simple, smooth and timeless</h1>
<!-- or, looping: -->
<h1 data-fade-up-stagger data-loop>Simple, smooth and timeless</h1>