← Text effects

Wave Text

Letters bob up and down in a travelling wave, like a flag in the breeze. It keeps going while the text is on screen and rests when it isn’t.

Riding the wave

How to use it

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

Options

Add any of these attributes to the same element.

data-heightHow high each letter rises, in em. Default 0.22.
data-speedTime for one bob. Default 1400 ms.
data-staggerGap between letters: smaller is a longer wave. Default 140 ms.
data-colorColour letters turn as the crest passes.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Wave Text: letters bob up and down in a travelling wave, like a flag in
    // the breeze. Keeps going while the text is on screen.
    // Usage: <h1 data-wave-text>Riding the wave</h1>
    // Optional: data-height (how high letters rise, in em; default 0.22),
    // data-speed (ms for one bob; default 1400), data-stagger (ms between
    // letters; default 140), data-color (letters take this colour at the crest).
    var DEFAULTS = { height: 0.22, speed: 1400, stagger: 140 };

    var css =
      '.wvt-word{display:inline-block;white-space:nowrap}' +
      '.wvt-char{display:inline-block;will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function split(el) {
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      var chars = [];
      text.split(' ').forEach(function (w, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var word = document.createElement('span');
        word.className = 'wvt-word';
        word.setAttribute('aria-hidden', 'true');
        Array.from(w).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'wvt-char';
          s.textContent = c;
          word.appendChild(s);
          chars.push(s);
        });
        el.appendChild(word);
      });
      return chars;
    }

    function start(el, chars) {
      var d = el.dataset;
      var amp = parseFloat(getComputedStyle(el).fontSize) * (+d.height || DEFAULTS.height);
      var speed = +d.speed || DEFAULTS.speed;
      var props = {
        y: [0, -amp, 0],
        duration: speed,
        ease: 'inOut(2)',
        loop: true,
        // Each letter starts a little later, so the crest travels along the line.
        delay: anime.stagger(+d.stagger || DEFAULTS.stagger)
      };
      if (d.color) {
        var base = getComputedStyle(el).color;
        props.color = [base, d.color, base];
      }
      return anime.animate(chars, props);
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-wave-text]').forEach(function (el) {
        if (el._wvt) return;
        el._wvt = true;
        var chars = split(el);
        var anim = null;
        // Only wave while visible, to save battery.
        new IntersectionObserver(function (entries) {
          if (!entries[0].isIntersecting) { if (anim) anim.pause(); return; }
          if (anim) anim.play();
          else anim = start(el, chars);
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-wave-text>Riding the wave</h1>

<!-- letters turn purple at the crest: -->
<h1 data-wave-text data-color="#a855f7">Riding the wave</h1>