← Text reveals

Character Rotate-in

Letters swing up into place around their baseline, as if hinged at the bottom, tipping just past upright before they settle. Loop it and they tip over backwards before swinging in again.

Swing into view

How to use it

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

Options

Add any of these attributes to the same element.

data-loopSwing in, hold, tip over backwards and swing in again.
data-holdHow long the text stays before tipping over, when looping. Default 1600 ms.
data-angleHow far forward the letters start tipped, in degrees. Default 100.
data-durationHow long each letter takes to swing up. Default 1100 ms.
data-staggerGap between letters. Default 40 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 Rotate-in: letters swing up into place around their baseline,
    // as if hinged at the bottom, tipping just past upright before settling.
    // Usage: <h1 data-character-rotate-in>Swing into view</h1>
    // Optional: data-loop (letters tip over backwards and swing in again),
    // data-hold (ms to stay before tipping over), data-angle (degrees letters
    // start tipped forward; default 100), data-duration (ms per letter),
    // data-stagger (ms between letters), data-delay (ms before starting).
    var DEFAULTS = { angle: 100, duration: 1100, stagger: 40, delay: 0, hold: 1600 };

    var css =
      '.cro-word{display:inline-block;white-space:nowrap;perspective:4em}' +
      '.cro-char{display:inline-block;transform-origin:50% 88%;backface-visibility:hidden;' +
      '-webkit-backface-visibility:hidden;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 = 'cro-word';
        w.setAttribute('aria-hidden', 'true');
        Array.from(word).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'cro-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 angle = +d.angle || DEFAULTS.angle;

      var tl = anime.createTimeline({ loop: loop, loopDelay: 300, delay: +d.delay || DEFAULTS.delay });
      tl.add(chars, {
        // Negative tips the top of the letter towards the reader.
        rotateX: { from: -angle, to: 0, ease: 'outBack(1.6)' },
        opacity: { from: 0, to: 1, duration: duration * 0.35, ease: 'linear' },
        duration: duration,
        delay: anime.stagger(stagger)
      });
      if (loop) {
        // Tip over backwards, away from the reader.
        tl.add(chars, {
          rotateX: angle,
          opacity: 0,
          duration: duration * 0.5,
          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._croChars);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-character-rotate-in]').forEach(function (el) {
        if (el._croChars) return;
        var chars = split(el);
        el._croChars = 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-rotate-in>Swing into view</h1>

<!-- or, looping: -->
<h1 data-character-rotate-in data-loop>Swing into view</h1>