← Text effects

Heartbeat Pulse

The text gives a double beat, a small one then a bigger one, rests, and beats again, like a pulse. Set the beats per minute, and an optional colour it flushes on each beat.

♥ Live now

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-heartbeat-pulse to a heading, badge or button.

Options

Add any of these attributes to the same element.

data-bpmBeats per minute. Default 72.
data-scaleSize at the bigger beat. Default 1.15.
data-colorA colour the text flushes on each beat.

It only beats while on screen. Great on “Live” badges and like counts.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Heartbeat Pulse: the text gives a double beat, a small one then a bigger
    // one, rests, and beats again, like a pulse. Keeps going while it's on
    // screen.
    // Usage: <h1 data-heartbeat-pulse>Live now</h1>
    // Optional: data-bpm (beats per minute; default 72), data-scale (size at
    // the bigger beat; default 1.15), data-color (a colour the text flushes on
    // each beat).
    var DEFAULTS = { bpm: 72, scale: 1.15 };

    var css = '.hbp-box{will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // What moves: buttons, badges and other boxes move as a whole. A block
    // heading's text goes in an inline box, so it moves around its own centre
    // rather than the middle of the full-width block.
    function build(el) {
      var display = getComputedStyle(el).display;
      if (display === 'inline') el.style.display = 'inline-block';
      if (display !== 'block') { el.classList.add('hbp-box'); return el; }
      var box = document.createElement('span');
      box.className = 'hbp-box';
      box.style.display = 'inline-block';
      while (el.firstChild) box.appendChild(el.firstChild);
      el.appendChild(box);
      return box;
    }

    function start(el, box) {
      var d = el.dataset;
      var big = +d.scale || DEFAULTS.scale;
      var small = 1 + (big - 1) * 0.55;
      // The beat itself takes 700 ms; the rest of each period is the pause.
      var period = 60000 / (+d.bpm || DEFAULTS.bpm);
      var props = {
        // Lub, a quick release, dub, then a slow ease back to rest.
        scale: [
          { to: small, duration: 110, ease: 'out(3)' },
          { to: 1, duration: 130, ease: 'in(2)' },
          { to: big, duration: 120, ease: 'out(3)' },
          { to: 1, duration: 340, ease: 'out(2)' }
        ],
        loop: true,
        loopDelay: Math.max(0, period - 700)
      };
      if (d.color) {
        var base = getComputedStyle(el).color;
        // The colour flushes in on the bigger beat and drains away with it.
        props.color = [
          { to: base, duration: 240 },
          { to: d.color, duration: 120, ease: 'out(3)' },
          { to: base, duration: 340, ease: 'out(2)' }
        ];
      }
      return anime.animate(box, props);
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-heartbeat-pulse]').forEach(function (el) {
        if (el._hbp) return;
        var box = (el._hbp = build(el));
        var anim = null;
        // Only beat 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, box);
        }, { threshold: 0.3 }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-heartbeat-pulse>Live now</h1>

<!-- slower, flushing pink: -->
<span data-heartbeat-pulse data-bpm="60" data-color="#ec4899">&hearts; 128</span>