← Text reveals

Character Flip-in

Letters tip forward into view in 3D, hinged along their top edge, like little flaps swinging down from behind the line. Loop it and they keep swinging towards you and vanish before flipping in again.

Flip into view

How to use it

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

Options

Add any of these attributes to the same element.

data-loopFlip in, hold, swing away and flip in again.
data-holdHow long the text stays before leaving, when looping. Default 1600 ms.
data-angleHow far back the letters start tipped, in degrees. Default 100.
data-durationHow long each letter takes to flip down. Default 1000 ms.
data-staggerGap between letters. Default 45 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 Flip-in: letters tip forward into view in 3D, hinged at their
    // top edge like little flaps swinging down from behind the line.
    // Usage: <h1 data-character-flip-in>Flip into view</h1>
    // Optional: data-loop (letters keep swinging towards you, vanish and flip
    // in again), data-hold (ms to stay before leaving), data-angle (degrees
    // letters start tipped back; default 100), data-duration (ms per letter),
    // data-stagger (ms between letters), data-delay (ms before starting).
    var DEFAULTS = { angle: 100, duration: 1000, stagger: 45, delay: 0, hold: 1600 };

    var css =
      '.cfi-word{display:inline-block;white-space:nowrap;perspective:3.5em}' +
      '.cfi-char{display:inline-block;transform-origin:50% 0;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 = 'cfi-word';
        w.setAttribute('aria-hidden', 'true');
        Array.from(word).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'cfi-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 swings the bottom of the letter away from the reader, so it
        // starts lying back behind its top edge and tips forward to upright.
        rotateX: { from: -angle, to: 0, ease: 'outBack(1.4)' },
        opacity: { from: 0, to: 1, duration: duration * 0.3, ease: 'linear' },
        duration: duration,
        delay: anime.stagger(stagger)
      });
      if (loop) {
        // Keep swinging forward, past the reader, and fade.
        tl.add(chars, {
          rotateX: 90,
          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._cfiChars);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-character-flip-in]').forEach(function (el) {
        if (el._cfiChars) return;
        var chars = split(el);
        el._cfiChars = 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-flip-in>Flip into view</h1>

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