← Text reveals

Character Scale-in

Letters grow from nothing and overshoot a touch before settling, rippling out from the middle of the text. Loop it and they swell, then shrink away from the outside in.

Make it pop

How to use it

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

Options

Add any of these attributes to the same element.

data-loopGrow in, hold, shrink away and grow again.
data-holdHow long the text stays before shrinking, when looping. Default 1600 ms.
data-fromWhere the ripple starts: center (default), first or last.
data-overshootHow far past full size the letters grow. Default 2.2; 0 for none.
data-durationHow long each letter takes to grow. Default 900 ms.
data-staggerGap between letters. Default 50 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 Scale-in: letters grow from nothing, overshooting a touch before
    // settling, rippling out from the middle of the text.
    // Usage: <h1 data-character-scale-in>Make it pop</h1>
    // Optional: data-loop (letters shrink away and grow again), data-hold (ms to
    // stay before shrinking), data-from (where the ripple starts: "center",
    // "first" or "last"), data-overshoot (how far past full size letters grow;
    // default 2.2, 0 for none), data-duration (ms per letter), data-stagger (ms
    // between letters), data-delay (ms before starting).
    var DEFAULTS = { from: 'center', overshoot: 2.2, duration: 900, stagger: 50, delay: 0, hold: 1600 };

    var css =
      '.csi-word{display:inline-block;white-space:nowrap}' +
      '.csi-char{display:inline-block;transform-origin:50% 70%;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 = 'csi-word';
        w.setAttribute('aria-hidden', 'true');
        Array.from(word).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'csi-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 from = ['first', 'last', 'center'].indexOf(d.from) > -1 ? d.from : DEFAULTS.from;
      var overshoot = d.overshoot !== undefined && d.overshoot !== '' ? +d.overshoot : DEFAULTS.overshoot;

      var tl = anime.createTimeline({ loop: loop, loopDelay: 300, delay: +d.delay || DEFAULTS.delay });
      tl.add(chars, {
        scale: { from: 0, to: 1, ease: overshoot > 0 ? 'outBack(' + overshoot + ')' : 'out(3)' },
        opacity: { from: 0, to: 1, duration: duration * 0.3, ease: 'linear' },
        duration: duration,
        delay: anime.stagger(stagger, { from: from })
      });
      if (loop) {
        // Swell slightly, then shrink back to nothing from the outside in.
        tl.add(chars, {
          scale: 0,
          duration: duration * 0.5,
          ease: 'inBack(1.4)',
          delay: anime.stagger(stagger * 0.6, { from: from, reversed: true })
        }, '+=' + (+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._csiChars);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-character-scale-in]').forEach(function (el) {
        if (el._csiChars) return;
        var chars = split(el);
        el._csiChars = chars;
        if (reduced()) return;
        anime.utils.set(chars, { scale: 0, opacity: 0 });
        io.observe(el);
      });
    }

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

<!-- left to right, looping: -->
<h1 data-character-scale-in data-from="first" data-loop>Make it pop</h1>