← Text effects

Breathing Glow

A soft glow swells around the text and fades away again, slow and even, like someone breathing in their sleep. The letters swell a touch as it breathes in.

Breathe in

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-breathing-glow to any heading or text element. It glows brightest on a dark background.

Options

Add any of these attributes to the same element.

data-colorColour of the glow. Default #a855f7.
data-durationOne whole breath, in and out. Default 5000 ms.
data-sizeHow far the glow spreads at its fullest, in em. Default 0.6.
data-scaleHow much the text swells as it breathes in. Default 1.03; 1 for none.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Breathing Glow: a soft glow swells around the text and fades away again,
    // slow and even like someone breathing in and out while they sleep.
    // Usage: <h1 data-breathing-glow>Breathe in</h1>
    // Optional: data-color (colour of the glow; default #a855f7), data-duration
    // (one whole breath, in and out, in ms; default 5000), data-size (how far
    // the glow spreads at its fullest, in em; default 0.6), data-scale (how
    // much the text swells as it breathes in; default 1.03, 1 for none).
    var DEFAULTS = { color: '#a855f7', duration: 5000, size: 0.6, scale: 1.03 };

    var css = '.bgl-on{display:inline-block;will-change:transform,text-shadow}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var color = d.color || DEFAULTS.color;
      var duration = +d.duration || DEFAULTS.duration;
      var size = d.size !== undefined ? +d.size : DEFAULTS.size;
      var scale = d.scale !== undefined ? +d.scale : DEFAULTS.scale;

      // The glow sits on an inner span so the element keeps its own display.
      var inner = document.createElement('span');
      inner.className = 'bgl-on';
      while (el.firstChild) inner.appendChild(el.firstChild);
      el.appendChild(inner);

      function c(alpha) { return 'color-mix(in srgb, ' + color + ' ' + Math.round(alpha * 100) + '%, transparent)'; }

      // b runs from 0 (breathed out) to 1 (lungs full).
      function paint(b) {
        var s = size * (0.25 + 0.75 * b);
        inner.style.textShadow =
          '0 0 ' + (s * 0.12).toFixed(3) + 'em ' + c(0.35 + 0.5 * b) + ',' +
          '0 0 ' + (s * 0.45).toFixed(3) + 'em ' + c(0.15 + 0.55 * b) + ',' +
          '0 0 ' + s.toFixed(3) + 'em ' + c(0.08 + 0.42 * b);
        inner.style.transform = 'scale(' + (1 + (scale - 1) * b).toFixed(4) + ')';
      }

      if (reduced()) { paint(0.7); return; }

      var breath = { b: 0 };
      paint(0);
      var tl = anime.createTimeline({ loop: true, autoplay: false, onUpdate: function () { paint(breath.b); } })
        // Breathe in, a beat at the top, a longer breath out, a pause.
        .add(breath, { b: 1, duration: duration * 0.4, ease: 'inOutSine' })
        .add(breath, { b: 1, duration: duration * 0.08 })
        .add(breath, { b: 0, duration: duration * 0.44, ease: 'inOutSine' })
        .add(breath, { b: 0, duration: duration * 0.08 });

      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) tl.play();
        else tl.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-breathing-glow]').forEach(function (el) {
        if (el._bgl) return;
        el._bgl = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-breathing-glow>Breathe in</h1>

<!-- a slow, wide, teal glow: -->
<h1 data-breathing-glow data-color="#2dd4bf" data-duration="8000" data-size="0.9">Relax</h1>