Spotlight Hover
A soft pool of light follows the pointer across a card, or across a whole grid of cards, glowing through their translucent backgrounds and borders.
Fast
Light
Simple
Yours
How to use it
- Paste the snippet just before
</head>. - Add
data-spotlight-hoverto a card, or to the container of a grid of cards.
Options
Add any of these attributes to the same element.
data-color | Colour of the light. Default soft purple. |
data-size | Radius of the light, in px. Default 260. |
data-autoplay | Drift on its own while the pointer isn’t over it. |
The light sits behind the content, so it works best on a dark section with cards that have a translucent background, e.g. rgba(255,255,255,.04).
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Spotlight Hover: a soft pool of light follows the pointer across a card,
// or across a whole grid of cards, glowing through anything translucent.
// Usage: <div class="card" data-spotlight-hover>…</div>
// or: <div class="grid" data-spotlight-hover>…cards…</div>
// Optional: data-color (default rgba(168,85,247,.45)), data-size (radius in
// px, default 260), data-autoplay (drift on its own while not hovered).
// The light sits behind the element's content, so give cards inside a grid a
// translucent background (e.g. rgba(255,255,255,.04)) to let it through.
var DEFAULTS = { color: 'rgba(168, 85, 247, 0.45)', size: 260 };
var css =
'.sph{position:relative;isolation:isolate}' +
'.sph>.sph-light{position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;opacity:0;' +
'background:radial-gradient(circle var(--sph-size) at var(--sph-x) var(--sph-y),var(--sph-color),transparent 70%)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var d = el.dataset;
el.classList.add('sph');
var light = document.createElement('div');
light.className = 'sph-light';
light.setAttribute('aria-hidden', 'true');
light.style.setProperty('--sph-color', d.color || DEFAULTS.color);
light.style.setProperty('--sph-size', (+d.size || DEFAULTS.size) + 'px');
el.insertBefore(light, el.firstChild);
var pos = { x: el.clientWidth / 2, y: el.clientHeight / 2 };
var apply = function () {
light.style.setProperty('--sph-x', pos.x + 'px');
light.style.setProperty('--sph-y', pos.y + 'px');
};
apply();
var hovering = false;
// Glide towards the pointer rather than snapping, so it feels like light.
function moveTo(x, y, duration) {
anime.animate(pos, { x: x, y: y, duration: duration, ease: 'out(3)', onUpdate: apply });
}
function fade(to) { anime.animate(light, { opacity: to, duration: 400, ease: 'out(2)' }); }
el.addEventListener('pointerenter', function (e) {
if (e.pointerType === 'touch') return;
hovering = true;
var r = el.getBoundingClientRect();
pos.x = e.clientX - r.left;
pos.y = e.clientY - r.top;
apply();
fade(1);
});
el.addEventListener('pointermove', function (e) {
if (!hovering) return;
var r = el.getBoundingClientRect();
moveTo(e.clientX - r.left, e.clientY - r.top, 450);
});
el.addEventListener('pointerleave', function () {
hovering = false;
if (d.autoplay === undefined) fade(0);
});
if (d.autoplay !== undefined && d.autoplay !== 'false') {
fade(1);
// A slow figure-of-eight drift while nobody is hovering.
var t = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
onUpdate: function () {
if (hovering) return;
t += 0.012;
var w = el.clientWidth, h = el.clientHeight;
pos.x = w * (0.5 + 0.38 * Math.sin(t));
pos.y = h * (0.5 + 0.32 * Math.sin(t * 2));
apply();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-spotlight-hover]').forEach(function (el) {
if (el._sph) return;
el._sph = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="grid" data-spotlight-hover>
<div class="card">…</div>
<div class="card">…</div>
<div class="card">…</div>
</div>