← Visual Library / Effects

Cursor-reveal spotlight

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.

Effects Built
Use when — hero sections that should feel alive and personal. Works for dark-tech aesthetics, AI/data brands, anywhere "there's something underneath" reads as a metaphor for the product. Skip on touch-only sites (effect doesn't work without a cursor).

Live demo

Move your cursor over the dark area below.

Move your cursor here

The green organism is hiding under the veil.

How it works — three layers

The container has isolation: isolate so the pseudo-elements stack inside it. Layer order, from back to front:

  1. ::before — the organism. Bright radial gradients (the thing you want revealed). Optionally animated.
  2. ::after — the veil. Solid color matching your section's intended look. A CSS mask punches a soft-edged hole at the cursor position.
  3. Content. Headlines, copy, CTAs — sits above both, always visible.

CSS

.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; }
}

JS — track the cursor

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');
});

Dials worth knowing

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.

Trap I hit

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."