Shadowboxing
I've been a fan of Jakub Krehel's approach to shadows for a while. He uses multi-layered box-shadows where you'd normally reach for a border. Shadows are transparent so they adapt to whatever background they land on, and borders can't. Once you've seen it you notice it everywhere.
I started using it in my own work, three shadow layers each doing a different job. An outline layer for definition, a directional layer for depth, and a softer ambient layer for atmosphere.
The cursor as a light source
At some point I wondered what would happen if you could move the light. What if the cursor was the light?
It's simpler than it sounds. Each element registers its centre position with a light engine. Every frame the engine works out the vector from the cursor to each element, then offsets and scales the shadow layers to match. Elements closer to the light get slightly sharper shadows, which is what Jakub does on hover, only 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 engine runs on requestAnimationFrame and writes straight to the DOM, so React never re-renders on mouse move. Each shadow-receiving element registers a ref and the engine handles the rest.
Try it
Move your mouse to reposition the light. The controls panel has shadow intensity, opacity, spread, colours and an inset toggle, all live.
Posted on 23 March 2026
Permalink, All Playground