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
- Paste the snippet just before
</head>. - Add
data-glow-on-hoverto a button, card or icon.
Options
Add any of these attributes to the same element.
data-color | Comma-separated glow colours. Default purple, pink and sky blue. |
data-size | How far the glow spreads, in px. Default 32. |
data-drop | Follow the element’s own outline instead of its box. Use it on icons and text. |
data-still | Keep the colours still instead of circling. |
data-duration | How long the bloom takes. Default 500 ms. |
data-autoplay | Elements 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.
<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><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>