webdevpuneet.com

Cursor Spotlight Reveal — Free CSS Mask Follow-the-Cursor Snippet

Cursor Spotlight Reveal · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="csr-panel" id="csrPanel">
  <div class="csr-dim">
    <h2>The full story is here</h2>
    <p>Move your cursor across this panel — everything beneath the spotlight becomes clear while the rest stays hidden in the dark. Try tracing the edges slowly.</p>
    <p class="csr-secret">Hidden line: the launch date is March 4th.</p>
  </div>
  <div class="csr-lit" id="csrLit">
    <h2>The full story is here</h2>
    <p>Move your cursor across this panel — everything beneath the spotlight becomes clear while the rest stays hidden in the dark. Try tracing the edges slowly.</p>
    <p class="csr-secret">Hidden line: the launch date is March 4th.</p>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real cursor tracking
mousemove coordinates drive the mask center directly.
CSS custom property mask
--csr-x/--csr-y feed a positioned radial-gradient.
Feathered spotlight edge
Gradient stops soften the circle's boundary.
No flash on move
Both layers always render; only the mask clips.
Clean mouseleave reset
The spotlight retreats off-panel, not frozen in place.
Touch-drag support
touchmove drives the same mask on mobile.
Pure CSS mask
No canvas, no image cutouts.
Easily resizable
One circle radius value controls spotlight size.

About this UI Snippet

Cursor Spotlight Reveal — A CSS Mask That Follows the Cursor

Screenshot of the Cursor Spotlight Reveal snippet rendered live

Two identical copies of the same content are stacked — one dim and always visible, one bright and hidden behind a circular CSS mask — so that only the area directly under the cursor reveals the vivid version underneath, like a flashlight sweeping a dark room. This snippet uses mask-image: radial-gradient(...) positioned by real mousemove coordinates, not a static hover effect.

Two stacked layers, one masked

The panel contains a .csr-dim layer (muted colors, always fully visible) and an identical .csr-lit layer (vivid colors, gradient text) positioned exactly on top of it with position: absolute; inset: 0. Because the content is duplicated rather than toggled, there is no flash or reflow when the mask moves — the bright version is always rendered, just clipped to a small circle by the mask.

The mask is a positioned radial-gradient

mask-image: radial-gradient(circle 120px at var(--csr-x) var(--csr-y), #000 55%, transparent 100%) draws a circle whose center comes from two CSS custom properties. A radial-gradient used as a mask makes its opaque stops (black, by mask convention) show the masked element and its transparent stops hide it — so the #000 55%, transparent 100% stop pair creates a solid spotlight core that feathers to nothing at its edge, rather than a hard-edged circle.

Real mousemove drives the mask, not a hover state

Every mousemove over the panel computes the cursor's position relative to the panel with getBoundingClientRect() and writes it straight into --csr-x/--csr-y via style.setProperty. Because the mask's at position references those same custom properties, the browser recomputes the gradient's center on every event — the spotlight is following the literal cursor coordinate every frame, not playing a fixed animation triggered by a hover class.

Leaving and touch

mouseleave resets the custom properties to an off-panel coordinate, so the spotlight disappears cleanly rather than freezing at its last position. A parallel touchmove/touchend pair gives the same behavior for a finger dragging across the panel on touch devices, since there is no persistent hover state to fall back on there.

Customizing it

Change the spotlight radius (both the circle 120px in CSS and nothing else needs to change, since JS only ever sets the center), swap the feather stop percentages for a harder or softer edge, or reveal an image instead of text. Pair it with a custom cursor for a themed pointer, or cursor text for a cursor-following label alongside the spotlight.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA dark panel renders with dim, barely-legible text.
  2. 2
    Move the cursor over the panelA circular spotlight reveals vivid text wherever it goes.
  3. 3
    Move the cursor awayThe spotlight fades off-panel and the text dims again.
  4. 4
    Drag a finger on touchThe same spotlight follows a finger instead of a mouse.
  5. 5
    Resize the spotlightChange the circle radius in the mask-image gradient.
  6. 6
    Swap the contentReplace the duplicated text with any bright/dim pair.

Real-world uses

Common Use Cases

Landing page reveals
Hide a headline or offer behind a dim layer that only a circular spotlight uncovers, for a sense of discovery.
Interactive storytelling
Reveal hidden details as visitors explore, using two stacked copies of the same content so nothing flashes as the mask moves.
Product teaser sections
Tease a feature list that only appears under the cursor, with --csr-x and --csr-y feeding a positioned radial gradient.
Portfolio hero sections
Pair with a custom cursor theme for a cohesive pointer-led experience, using feathered edges for a soft torch effect.
CSS masking reference
Learn how mask-image and a mousemove handler combine, which is useful for highlighting the exact area a user is pointing at in a dark dashboard.
Related: Flip Countdown
See the Flip Countdown for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A mousemove listener on the panel computes the cursor's position relative to the panel using getBoundingClientRect(), then writes that x and y straight into two CSS custom properties via style.setProperty. The mask-image's radial-gradient references those same custom properties as its center point, so the browser recomputes the gradient's position on every mousemove event — the mask genuinely tracks the live cursor coordinate rather than playing a fixed hover animation.

A dim, always-visible layer sits behind an identical bright layer that has the CSS mask applied. Because both layers render continuously and only the top layer is clipped by the mask, moving the spotlight never causes a flash, reflow, or content swap — it simply reveals more or less of the already-rendered bright layer beneath the mask's opaque area.

mask-image: radial-gradient(circle 120px at X Y, #000 55%, transparent 100%) draws a circle 120px in radius; opaque (black) up to 55% of that radius, then fading to fully transparent by 100%. Since mask opacity follows the gradient's own alpha/lightness, that stop pair produces a solid spotlight core with a soft feathered edge rather than a hard-edged circle. Moving the 55% stop closer to 100% sharpens the edge; moving it toward 0% softens it further.

Yes — a parallel touchmove listener reads the first active touch point and writes the same custom properties the mouse handler does, so dragging a finger across the panel moves the spotlight identically. A touchend handler resets the mask off-panel, mirroring the mouseleave behavior, since touch has no hover state to fall back on.

Attach the mousemove/mouseleave (and touchmove/touchend) listeners to the panel's ref in a mount effect, and set the CSS custom properties directly via ref.current.style.setProperty rather than through component state, since updating on every mousemove through state would cause excessive re-renders. Clean up the listeners on unmount.