← Text reveals

Character Drop

Letters fall into place from above, one after another, and land with a small bounce, straightening up as they settle. Loop it and they drop through the floor before falling in again.

Drop it in

How to use it

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

Options

Add any of these attributes to the same element.

data-loopDrop in, hold, fall away and drop in again.
data-holdHow long the text stays before falling away, when looping. Default 1600 ms.
data-heightHow far the letters fall, in em. Default 1.
data-durationHow long each letter takes to land. Default 1000 ms.
data-staggerGap between letters. Default 55 ms.
data-delayWait before the first letter. Default 0 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Character Drop: letters fall into place from above, one after another,
    // landing with a small bounce and straightening up as they settle.
    // Usage: <h1 data-character-drop>Drop it in</h1>
    // Optional: data-loop (letters fall away and drop in again), data-hold (ms
    // to stay before falling away), data-height (how far letters fall, in em;
    // default 1), data-duration (ms per letter), data-stagger (ms between
    // letters), data-delay (ms before starting).
    var DEFAULTS = { height: 1, duration: 1000, stagger: 55, delay: 0, hold: 1600 };

    var css =
      '.cdr-word{display:inline-block;white-space:nowrap}' +
      '.cdr-char{display:inline-block;transform-origin:50% 100%;will-change:transform}';
    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 = '';
      var chars = [];
      text.split(' ').forEach(function (word, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var w = document.createElement('span');
        w.className = 'cdr-word';
        w.setAttribute('aria-hidden', 'true');
        Array.from(word).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'cdr-char';
          s.textContent = c;
          w.appendChild(s);
          chars.push(s);
        });
        el.appendChild(w);
      });
      return chars;
    }

    function play(el, chars) {
      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 height = (+d.height || DEFAULTS.height) + 'em';

      var tl = anime.createTimeline({ loop: loop, loopDelay: 300, delay: +d.delay || DEFAULTS.delay });
      tl.add(chars, {
        y: { from: '-' + height, to: '0em', ease: 'outBounce' },
        // Each letter starts a little crooked and lands straight.
        rotate: { from: function () { return anime.utils.random(-14, 14); }, to: 0, ease: 'out(3)' },
        opacity: { from: 0, to: 1, duration: duration * 0.25, ease: 'linear' },
        duration: duration,
        delay: anime.stagger(stagger)
      });
      if (loop) {
        // Fall away through the floor, so the next drop reads as new letters.
        tl.add(chars, {
          y: '0.6em',
          opacity: 0,
          duration: duration * 0.45,
          ease: 'in(3)',
          delay: anime.stagger(stagger * 0.6)
        }, '+=' + (+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._cdrChars);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-character-drop]').forEach(function (el) {
        if (el._cdrChars) return;
        var chars = split(el);
        el._cdrChars = chars;
        if (reduced()) return;
        anime.utils.set(chars, { opacity: 0 });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-character-drop>Drop it in</h1>

<!-- or, looping: -->
<h1 data-character-drop data-loop>Drop it in</h1>