Cursor Light Source
The pointer becomes a lamp. A warm glow lights up the surface around it, and the elements on the page cast soft shadows away from it, growing longer and blurrier as the light moves further off.
Lamplight
How to use it
- Paste the snippet just before
</head>. - Add
data-cursor-light-sourceto a section. - Add
data-shadowto the elements that should cast shadows. With none marked, every direct child does. A number sets how high it floats:data-shadow="2"throws a shadow twice as long.
Options
Add any of these attributes to the same element.
data-length | The longest a shadow gets, in px. Default 48. |
data-shadow-color | Shadow colour. Default rgba(40,20,80,.42). |
data-glow | Colour of the light on the surface. Default a warm white; none for no glow and no bulb. |
data-autoplay | The light drifts round the section while a real pointer isn’t in it. Handy for demos and touch screens. |
Shadows follow the shape of each element, transparent corners and all, because they’re drawn with drop-shadow. It looks best on a slightly dim surface, so the glow shows. On touch screens, the light follows your finger. With reduced motion turned on, the light jumps to the pointer without gliding.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Cursor Light Source: the pointer becomes a lamp. Elements in the area
// cast soft shadows away from it, longer and blurrier the further off the
// light is, and a glow lights up the surface around the pointer.
// Usage: <section data-cursor-light-source>
// <div class="card" data-shadow>…</div>
// <h2 data-shadow="2">…</h2>
// </section>
// Elements marked data-shadow cast shadows; with none marked, every direct
// child does. A number on data-shadow sets how high it floats (default 1):
// higher things throw longer shadows.
// Optional: data-length (longest shadow, px, default 48), data-shadow-color
// (default rgba(40,20,80,.42)), data-glow (colour of the light on the
// surface, default rgba(255,248,225,.95); "none" for no glow and no bulb),
// data-autoplay (the light drifts around while a real pointer isn't in the
// area; handy for demos and touch screens).
var DEFAULTS = { length: 48, shadowColor: 'rgba(40,20,80,.42)', glow: 'rgba(255,248,225,.95)' };
var css =
'.cln{position:relative;isolation:isolate;overflow:hidden}' +
'.cln-glow{position:absolute;inset:0;z-index:-1;pointer-events:none}' +
'.cln-bulb{position:absolute;left:0;top:0;z-index:5;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;pointer-events:none;' +
'background:#fffbe8;box-shadow:0 0 6px 2px #fff6c9,0 0 22px 8px rgba(255,214,102,.65);transition:opacity .3s}' +
'.cln-cast{will-change:filter}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
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 maxLen = num(d.length, DEFAULTS.length);
var shade = d.shadowColor || DEFAULTS.shadowColor;
var glowColour = d.glow || DEFAULTS.glow;
var glowOn = glowColour !== 'none';
var auto = d.autoplay !== undefined && d.autoplay !== 'false';
var still = reduced();
el.classList.add('cln');
var casters = el.querySelectorAll('[data-shadow]');
if (!casters.length) casters = el.children;
casters = Array.prototype.slice.call(casters).map(function (c) {
c.classList.add('cln-cast');
return { el: c, lift: num(c.dataset ? c.dataset.shadow : '', 1) };
});
var glow, bulb;
if (glowOn) {
glow = document.createElement('div');
glow.className = 'cln-glow';
glow.setAttribute('aria-hidden', 'true');
el.insertBefore(glow, el.firstChild);
bulb = document.createElement('div');
bulb.className = 'cln-bulb';
bulb.setAttribute('aria-hidden', 'true');
el.appendChild(bulb);
}
var w = 0, h = 0;
var light = { x: 0, y: 0 }, target = { x: 0, y: 0 };
var hover = false, hold = 0, visible = false, raf = 0, t = Math.random() * 6;
function render() {
var b = el.getBoundingClientRect();
w = b.width; h = b.height;
var reach = Math.max(w, h);
for (var i = 0; i < casters.length; i++) {
var c = casters[i], r = c.el.getBoundingClientRect();
var cx = r.left + r.width / 2 - b.left, cy = r.top + r.height / 2 - b.top;
var dx = cx - light.x, dy = cy - light.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
// Shadow length grows with distance from the light, up to a limit.
var len = Math.min(maxLen * c.lift, dist * 0.16 * c.lift);
var ox = dx / dist * len, oy = dy / dist * len;
var blur = 3 + len * 0.45;
var fade = Math.max(0.35, 1 - dist / (reach * 1.4));
c.el.style.filter = 'drop-shadow(' + ox.toFixed(1) + 'px ' + oy.toFixed(1) + 'px ' + blur.toFixed(1) + 'px ' + shade + ')' +
(fade < 0.99 ? ' brightness(' + (0.86 + 0.14 * fade).toFixed(3) + ')' : '');
}
if (glowOn) {
glow.style.background = 'radial-gradient(circle at ' + light.x.toFixed(1) + 'px ' + light.y.toFixed(1) + 'px,' + glowColour + ',transparent ' + Math.round(reach * 0.55) + 'px)';
bulb.style.transform = 'translate(' + light.x.toFixed(1) + 'px,' + light.y.toFixed(1) + 'px)';
}
}
function frame() {
raf = 0;
if (!visible) return;
var fake = auto && !hover && Date.now() > hold;
if (fake) {
// A slow drifting orbit, dipping close to the elements now and then.
t += 0.008;
target.x = w * (0.5 + 0.42 * Math.cos(t));
target.y = h * (0.5 + 0.38 * Math.sin(t * 1.6));
} else if (!hover && Date.now() > hold) {
target.x = w * 0.5; target.y = h * 0.12;
}
var k = still ? 1 : 0.14;
var mx = (target.x - light.x) * k, my = (target.y - light.y) * k;
light.x += mx; light.y += my;
render();
if (fake || Math.abs(mx) + Math.abs(my) > 0.05) raf = requestAnimationFrame(frame);
}
function wake() { if (!raf && visible) raf = requestAnimationFrame(frame); }
function aim(e) {
var b = el.getBoundingClientRect();
target.x = e.clientX - b.left; target.y = e.clientY - b.top;
wake();
}
el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') { hover = true; aim(e); } });
el.addEventListener('pointermove', function (e) {
if (e.pointerType === 'touch') hold = Date.now() + 2500;
else hover = true;
aim(e);
});
el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch') { hover = false; wake(); } });
el.addEventListener('pointerdown', function (e) {
if (e.pointerType !== 'touch') return;
hold = Date.now() + 2500;
aim(e);
});
window.addEventListener('scroll', wake, { passive: true });
var b0 = el.getBoundingClientRect();
w = b0.width; h = b0.height;
light.x = target.x = w * (auto && !still ? 0.92 : 0.5);
light.y = target.y = h * (auto && !still ? 0.5 : 0.12);
if (still) auto = false;
render();
new ResizeObserver(function () { var b = el.getBoundingClientRect(); w = b.width; h = b.height; render(); wake(); }).observe(el);
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) wake();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-cursor-light-source]').forEach(function (el) {
if (el._cln) return;
el._cln = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-cursor-light-source>
<div class="card" data-shadow>…</div>
<img src="mug.png" alt="" data-shadow="2">
</section>