A solid veil hides a vibrant layer underneath. As the cursor moves, a soft-edged hole follows it — revealing the layer below like a flashlight in a dark room.
Move your cursor over the dark area below.
The green organism is hiding under the veil.
The container has isolation: isolate so the pseudo-elements stack inside it. Layer order, from back to front:
::before — the organism. Bright radial gradients (the thing you want revealed). Optionally animated.::after — the veil. Solid color matching your section's intended look. A CSS mask punches a soft-edged hole at the cursor position..hero {
position: relative;
isolation: isolate;
overflow: hidden;
/* JS-driven cursor position */
--mx: -300px;
--my: -300px;
}
/* Vibrant layer underneath */
.hero::before {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(circle at 22% 28%, rgba(110,231,183,0.7), transparent 38%),
radial-gradient(circle at 78% 72%, rgba(16,185,129,0.65), transparent 38%);
filter: blur(24px);
z-index: 0;
}
/* The veil that hides it, with a cursor-tracked hole */
.hero::after {
content: "";
position: absolute; inset: 0;
background: var(--navy-deep); /* the "off" state color */
z-index: 1;
mask: radial-gradient(circle 160px at var(--mx) var(--my),
transparent 0%, transparent 22%,
rgba(0,0,0,0.85) 70%, black 100%);
-webkit-mask: radial-gradient(circle 160px at var(--mx) var(--my),
transparent 0%, transparent 22%,
rgba(0,0,0,0.85) 70%, black 100%);
}
.hero-content { position: relative; z-index: 2; }
/* Touch devices — no cursor to track, drop the reveal */
@media (hover: none) {
.hero::after { mask: none; -webkit-mask: none; }
}
const hero = document.querySelector('.hero');
hero.addEventListener('pointermove', (e) => {
const r = hero.getBoundingClientRect();
hero.style.setProperty('--mx', (e.clientX - r.left) + 'px');
hero.style.setProperty('--my', (e.clientY - r.top) + 'px');
});
hero.addEventListener('pointerleave', () => {
hero.style.setProperty('--mx', '-300px');
hero.style.setProperty('--my', '-300px');
});
Spotlight size — change circle 160px. Smaller = more focused, intimate. Larger = wider window, more obvious.
Falloff — adjust the mask gradient stops. transparent 0% 22% means the inner core (22% of radius) is fully clear; the rest fades in to opaque. Tighter stops = sharper edge.
What's under the veil — gradients work, but you can also use an image, a video, or an animated organism (drifting radial gradients on a slow keyframe). The veil hides everything until cursor passes.
No-hover devices — on touch screens drop the mask and let the veil read as a soft static aurora instead. Don't try to fake the effect on tap.
Setting CSS variables via JS on the :root (or document.body) feels tempting but cascade-wise the variable belongs to the element with the mask. Set it on the hero itself. Otherwise pointer events on other elements muddy the position.
Source: built for jc-data-automation-site, 2026-05-19. Spotlight radius reduced from 280px → 160px after Julian flagged the original as "too big the area it reveals."