Anatomy of Spotlight Reveal
How an inverted radial mask punches a hole through a cover layer, eased toward the pointer on a single rAF loop — with idle breathing and click-to-pin.
SpotlightReveal stacks two full-bleed layers. The reveal prop sits
underneath; children is the cover. An inverted radial mask on the cover
makes a soft hole that follows the pointer — exposing whatever's beneath
without unmounting either layer.
Reveal under, cover on top
Both layers are absolute inset-0. The cover owns the mask; the reveal
is always painted and simply shows through the hole. Clicking the root
toggles a pinned ref so the spotlight can freeze in place.
reveal
cover
masked
- Reveal
- absolute inset-0 underneath — the hidden layer
- Cover
- children on top, punched by the radial mask
- Together
- mask hole follows --x/--y on the cover
<div className="absolute inset-0">{reveal}</div>
<div
ref={coverRef}
className="absolute inset-0"
style={
reduce
? { opacity: 0.9 }
: { WebkitMaskImage: maskImage, maskImage }
}
>
{children}
</div>Softness is the hard edge
softness (0–1, default 0.55) sets how wide the feather is.
inner = radius * (1 - softness) is the fully transparent core; from
there the mask ramps to opaque at radius. Hard edges keep the hole
crisp; soft ones feel like a flashlight bloom.
transparent 0 → transparent inner → #000 radius
- Hard edge
- softness → 0 · inner ≈ radius
- Soft feather
- softness 0.55 · inner = radius × 0.45
const inner = Math.max(0, radius * (1 - softness));
const maskImage =
`radial-gradient(circle ${radius}px at var(--x, 50%) var(--y, 50%), ` +
`transparent 0, transparent ${inner}px, #000 ${radius}px)`;Eased follow, idle breathing, pin
A single requestAnimationFrame loop owns motion. Each frame:
- If idle (
!hovering && !pinned), retarget onto a slow ellipse —cos(t·0.6)·18%w/sin(t·0.8)·16%haround center. - Ease:
cur += (target − cur) * ease(default0.16). - Write
--x/--yon the cover withsetProperty.
Pointer enter/move updates target (unless pinned). Click flips
pinned. No React state for position — same cheap path as
SpotlightCard, plus the lag that makes the light feel liquid.
cur.x += (target.x - cur.x) * ease
- Idle drift
- ellipse when !hovering && !pinned
- Eased follow
- cur += (target − cur) × 0.16 per frame
- Pinned
- click toggles — target freezes until unpin
cur.current.x += (target.current.x - cur.current.x) * ease;
cur.current.y += (target.current.y - cur.current.y) * ease;
cover.style.setProperty("--x", `${cur.current.x}px`);
cover.style.setProperty("--y", `${cur.current.y}px`);Reduced motion
useReducedMotion() skips the rAF entirely. The cover drops the mask and
sits at opacity: 0.9 — both layers readable as a calm blend, no
breathing, no chase.
style={
reduce
? { opacity: 0.9 }
: { WebkitMaskImage: maskImage, maskImage }
}The result
Two layers, one inverted mask, a 0.16 ease, and an idle ellipse that invites the hover. Move to follow; click to pin.
GODUI
Cover layer
Move to reveal