← Text reveals

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

  1. Paste the snippet just before </head>.
  2. Add data-fade-up-stagger to any heading or text element.

Options

Add any of these attributes to the same element.

data-loopFade in, hold, fade up and away, and in again.
data-holdHow long the text stays before fading away, when looping. Default 1800 ms.
data-distanceHow far the words lift, in em. Default 0.6.
data-durationHow long each word takes to arrive. Default 900 ms.
data-staggerGap between words. Default 80 ms.
data-delayWait before the first word. Default 0 ms.
1 · Snippet
<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>
2 · Your HTML
<h1 data-fade-up-stagger>Simple, smooth and timeless</h1>

<!-- or, looping: -->
<h1 data-fade-up-stagger data-loop>Simple, smooth and timeless</h1>