Shadowboxing
I've been a fan of Jakub Krehel's approach to shadows for a while. His idea is simple: use multi-layered box-shadows instead of borders. Shadows adapt to any background because they use transparency. Borders don't. Once you see the difference, it's hard to go back.
I started adopting this in my own work — three shadow layers, each with a different purpose. An outline layer for definition. A subtle directional layer for depth. A softer ambient layer for atmosphere.
The cursor as a light source
At some point I had a thought: if shadows imply a light source, what if you could actually move it? What if the cursor became the light?
The implementation is straightforward. Each element registers its centre position with a light engine. On every frame, the engine computes the vector from the cursor (light) to each element, then offsets and scales the shadow layers accordingly. Elements closer to the light get slightly sharper shadows - the same way Jakub bumps opacity on hover, but continuous.
const LAYERS = [
{ offset: 0, blur: 0, spread: 1, opacityScale: 1 }, // outline
{ offset: 1, blur: 2, spread: -1, opacityScale: 1 }, // directional
{ offset: 2, blur: 4, spread: 0, opacityScale: 0.66 }, // ambient
];
// Vector from light to element — shadow casts in this direction
const dx = elCx - light.x;
const dy = elCy - light.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Closer to light = slightly sharper
const proximity = 1 / (1 + dist * 1.5);The light engine runs on requestAnimationFrame and writes directly to the DOM — no React re-renders on mouse move. Each shadow-receiving element just registers a ref and the engine handles the rest.
Try it
The demo below is fully interactive. Move your mouse to reposition the light source. Open the controls panel to tweak shadow intensity, opacity, spread, colours, and toggle inset shadows. Everything updates in real time.
Posted on 23 March 2026
Permalink, All Playground