← Rotating words

Word Flip

The word in your sentence flips over on its horizontal axis, like a sign turned on a hinge, to show the next word on its back. It lifts towards you as it turns and lands with a small bounce.

Ideas that stick

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the changing word in a <span> and list every word, separated by commas, in data-word-flip.

Options

Add any of these attributes to the same element.

data-holdHow long each word stays. Default 1800 ms.
data-durationTime for each flip. Default 900 ms.
data-directionup (default): the top edge tips away from you. down: it tips towards you.

The word is as wide as the longest one, so the sentence never shifts.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Word Flip: the word in your sentence flips over on its horizontal axis,
    // like a sign turned on a hinge, to show the next word on its back. It
    // lifts slightly as it turns and lands with a small bounce.
    // Usage: <h1>Ideas that <span data-word-flip="stick, sell, scale">stick</span></h1>
    // Optional: data-hold (ms per word, default 1800), data-duration (ms per
    // flip, default 900), data-direction ("up" (default): the top edge tips
    // away from you; "down": it tips towards you).
    var DEFAULTS = { hold: 1800, duration: 900 };

    var css =
      '.wfl{display:inline-grid;position:relative;vertical-align:baseline;text-align:left;perspective:5em}' +
      '.wfl-size{grid-area:1/1;visibility:hidden;white-space:nowrap}' +
      '.wfl-card{position:absolute;inset:0;transform-style:preserve-3d}' +
      '.wfl-face{position:absolute;inset:0;white-space:nowrap;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
      '.wfl-back{transform:rotateX(180deg)}' +
      '.wfl-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function span(cls, parent, text) {
      var s = document.createElement('span');
      s.className = cls;
      if (text !== undefined) s.textContent = text;
      parent.appendChild(s);
      return s;
    }

    function build(el) {
      var words = (el.getAttribute('data-word-flip') || el.textContent)
        .split(',').map(function (w) { return w.trim(); }).filter(Boolean);
      el.classList.add('wfl');
      el.textContent = '';
      span('wfl-sr', el, words.join(', '));
      // Hidden copies of every word: the box takes the widest one and sits on the
      // sentence's baseline, so nothing around it shifts.
      words.forEach(function (w) { span('wfl-size', el, w).setAttribute('aria-hidden', 'true'); });
      var card = span('wfl-card', el);
      card.setAttribute('aria-hidden', 'true');
      var faces = [span('wfl-face', card, words[0]), span('wfl-face wfl-back', card, '')];
      return { el: el, words: words, card: card, faces: faces, step: 0 };
    }

    function start(s) {
      var d = s.el.dataset;
      var dir = d.direction === 'down' ? -1 : 1;
      var duration = +d.duration || DEFAULTS.duration;
      (function next() {
        s.step++;
        // The face turning away from you gets the next word written on it.
        s.faces[s.step % 2].textContent = s.words[s.step % s.words.length];
        anime.createTimeline({ delay: +d.hold || DEFAULTS.hold, onComplete: next })
          .add(s.card, { rotateX: dir * s.step * 180, duration: duration, ease: 'outBack(1.7)' }, 0)
          // A little lift towards you mid-flip, so it feels like a real card.
          .add(s.card, { z: [{ to: '0.35em', duration: duration * 0.35, ease: 'out(2)' }, { to: 0, duration: duration * 0.5, ease: 'inOut(2)' }] }, 0)
          // The faces darken slightly as they turn edge-on to the light.
          .add(s.faces, { filter: ['brightness(1)', 'brightness(0.6)', 'brightness(1)'], duration: duration * 0.65, ease: 'inOut(2)' }, 0);
      })();
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          start(e.target._wfl);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-word-flip]').forEach(function (el) {
        if (el._wfl) return;
        el._wfl = build(el);
        // Reduced motion: the first word simply stays.
        if (!reduced && el._wfl.words.length > 1) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>
  Ideas that
  <span data-word-flip="stick, sell, scale">stick</span>
</h1>