← Hover & cursor

Glow on Hover

Hover a button or card and a soft coloured glow blooms out from behind it, its colours slowly circling round the edges. Move away and the glow sinks back into the dark.

Pro$12per month

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-glow-on-hover to a button, card or icon.

Options

Add any of these attributes to the same element.

data-colorComma-separated glow colours. Default purple, pink and sky blue.
data-sizeHow far the glow spreads, in px. Default 32.
data-dropFollow the element’s own outline instead of its box. Use it on icons and text.
data-stillKeep the colours still instead of circling.
data-durationHow long the bloom takes. Default 500 ms.
data-autoplayElements that share a parent take turns glowing while the pointer isn’t on any. Handy for demos.

It looks best on a dark background. Any shadow you’ve given the element is kept underneath the glow. A press lights it on touch screens, and keyboard focus does too.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Glow on Hover: hover a button or card and a soft coloured glow blooms out
    // from behind it, its colours slowly circling round the edges. Move away
    // and the glow sinks back.
    // Usage: <button data-glow-on-hover>Get started</button>
    // Optional: data-color (comma-separated glow colours, default purple, pink
    // and sky blue), data-size (how far the glow spreads, px, default 32),
    // data-drop (follow the element's own outline, for icons and text, instead
    // of its box), data-still (don't circle the colours), data-duration (ms for
    // the bloom, default 500), data-autoplay (elements that share a parent take
    // turns glowing while the pointer isn't on any; handy for demos and touch
    // screens).
    // It looks best on a dark background.
    var DEFAULTS = { color: '#a855f7,#ec4899,#38bdf8', size: 32, duration: 500 };

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function num(v, def) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : def; }

    function setup(el) {
      var d = el.dataset;
      var colours = (d.color || DEFAULTS.color).split(',').map(function (c) { return c.trim(); });
      var size = num(d.size, DEFAULTS.size);
      var duration = num(d.duration, DEFAULTS.duration);
      var drop = d.drop !== undefined && d.drop !== 'false';
      var still = (d.still !== undefined && d.still !== 'false') || reduced();
      var own = drop ? getComputedStyle(el).filter : getComputedStyle(el).boxShadow;
      own = own && own !== 'none' ? own : '';

      var s = { k: 0, t: Math.random() * 6 };
      var on = false, real = false, hold = 0, raf = 0, anim = null, release = null;

      // Each colour sits a little off-centre and they circle round, so the
      // glow shifts hue around the edges.
      function draw() {
        var k = s.k, n = colours.length, parts = [];
        for (var i = 0; i < n; i++) {
          var a = s.t + (i * Math.PI * 2) / n;
          var off = size * 0.28 * k;
          var x = (Math.cos(a) * off).toFixed(1), y = (Math.sin(a) * off).toFixed(1);
          if (drop) parts.push('drop-shadow(' + x + 'px ' + y + 'px ' + (size * 0.45 * k).toFixed(1) + 'px ' + colours[i] + ')');
          else parts.push(x + 'px ' + y + 'px ' + (size * k).toFixed(1) + 'px ' + (size * 0.12 * k).toFixed(1) + 'px ' + colours[i]);
        }
        if (!drop) parts.push('0 0 ' + (size * 2.2 * k).toFixed(1) + 'px ' + (size * 0.2 * k).toFixed(1) + 'px ' + colours[0]);
        if (k < 0.002) {
          if (drop) el.style.filter = '';
          else el.style.boxShadow = '';
          return;
        }
        if (drop) el.style.filter = (own ? own + ' ' : '') + parts.join(' ');
        else el.style.boxShadow = (own ? own + ',' : '') + parts.join(',');
      }
      function frame() {
        raf = 0;
        if (!still) s.t += 0.028;
        draw();
        if (on || s.k > 0.002) raf = requestAnimationFrame(frame);
      }
      function wake() { if (!raf) raf = requestAnimationFrame(frame); }

      function enter() {
        if (on) return;
        on = true;
        if (anim) anim.pause();
        anim = anime.animate(s, { k: 1, duration: reduced() ? 0 : duration, ease: 'out(3)' });
        wake();
      }
      function leave() {
        if (!on) return;
        on = false;
        if (anim) anim.pause();
        anim = anime.animate(s, { k: 0, duration: reduced() ? 0 : duration * 1.4, ease: 'inOut(2)' });
        wake();
      }

      el.addEventListener('pointerenter', function (e) {
        if (e.pointerType === 'touch') return;
        real = true;
        enter();
      });
      el.addEventListener('pointerleave', function (e) {
        if (e.pointerType === 'touch') return;
        real = false;
        leave();
      });
      // Touch: it glows under the finger and fades a moment after.
      el.addEventListener('pointerdown', function (e) {
        if (e.pointerType !== 'touch') return;
        hold = Date.now() + 4000;
        clearTimeout(release);
        enter();
      });
      function up(e) {
        if (e.pointerType !== 'touch') return;
        clearTimeout(release);
        release = setTimeout(leave, 900);
      }
      el.addEventListener('pointerup', up);
      el.addEventListener('pointercancel', up);
      el.addEventListener('focusin', enter);
      el.addEventListener('focusout', function (e) { if (!el.contains(e.relatedTarget)) leave(); });

      return {
        enter: enter,
        leave: leave,
        busy: function () { return real || el.contains(document.activeElement) || Date.now() < hold; }
      };
    }

    // Elements that share a parent take turns, one at a time.
    function join(el, api) {
      var host = el.parentNode;
      var g = host._gohGroup;
      if (g) { g.items.push(api); return; }
      g = host._gohGroup = { items: [api], i: 0 };
      var visible = false, timer = null, cur = null;
      function tick() {
        if (!visible) return;
        if (g.items.some(function (a) { return a.busy(); })) {
          if (cur && !cur.busy()) cur.leave();
          cur = null;
          timer = setTimeout(tick, 800);
          return;
        }
        if (cur) { cur.leave(); cur = null; timer = setTimeout(tick, 500); return; }
        cur = g.items[g.i++ % g.items.length];
        cur.enter();
        timer = setTimeout(tick, 1700);
      }
      new IntersectionObserver(function (entries) {
        var was = visible;
        visible = entries[0].isIntersecting;
        if (visible && !was) { clearTimeout(timer); timer = setTimeout(tick, 400); }
      }).observe(host);
    }

    function init() {
      document.querySelectorAll('[data-glow-on-hover]').forEach(function (el) {
        if (el._goh) return;
        el._goh = true;
        var api = setup(el);
        var auto = el.dataset.autoplay;
        if (auto === undefined || auto === 'false' || reduced()) return;
        join(el, api);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-glow-on-hover>Get started</button>

<!-- a warm glow that follows an icon's shape: -->
<svg data-glow-on-hover data-drop data-color="#fbbf24,#f97316">…</svg>