Cursor Spotlight Reveal — Free CSS Mask Follow-the-Cursor Snippet
Cursor Spotlight Reveal · Animations · Plain HTML, CSS & JS · Live preview
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>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#050608;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:22px}
.csr-panel{position:relative;width:100%;max-width:620px;min-height:280px;border-radius:18px;overflow:hidden;background:#0a0c12;border:1px solid #1a1e29;cursor:none}
.csr-dim,.csr-lit{position:absolute;inset:0;padding:34px;display:flex;flex-direction:column;justify-content:center;gap:14px}
.csr-dim{color:#262b38}
.csr-dim h2{font-size:24px;letter-spacing:-.01em}
.csr-dim p{font-size:14.5px;line-height:1.6}
.csr-secret{font-weight:700;color:#333}
.csr-lit{color:#f5f3ff}
.csr-lit h2{font-size:24px;letter-spacing:-.01em;background:linear-gradient(120deg,#c4b5fd,#f0abfc);-webkit-background-clip:text;background-clip:text;color:transparent}
.csr-lit p{font-size:14.5px;line-height:1.6;color:#d8d3ee}
.csr-secret{font-weight:700}
.csr-lit .csr-secret{color:#f0abfc}
/* The spotlight mask: a radial-gradient sized to a fixed spotlight radius,
positioned via CSS custom properties updated on every mousemove — only
the area within the gradient's opaque stop is visible. */
.csr-lit{
-webkit-mask-image: radial-gradient(circle 120px at var(--csr-x, -200px) var(--csr-y, -200px), #000 55%, transparent 100%);
mask-image: radial-gradient(circle 120px at var(--csr-x, -200px) var(--csr-y, -200px), #000 55%, transparent 100%);
transition: -webkit-mask-image .02s linear;
}var panel = document.getElementById('csrPanel');
var lit = document.getElementById('csrLit');
// Real mousemove tracking drives the mask's center position via CSS custom
// properties — the spotlight literally follows wherever the cursor is, it
// is not a canned hover-state animation.
function moveSpotlight(clientX, clientY) {
var rect = panel.getBoundingClientRect();
var x = clientX - rect.left;
var y = clientY - rect.top;
panel.style.setProperty('--csr-x', x + 'px');
panel.style.setProperty('--csr-y', y + 'px');
}
panel.addEventListener('mousemove', function (e) {
moveSpotlight(e.clientX, e.clientY);
});
panel.addEventListener('mouseleave', function () {
panel.style.setProperty('--csr-x', '-200px');
panel.style.setProperty('--csr-y', '-200px');
});
// Touch support: the spotlight follows a finger dragging across the panel.
panel.addEventListener('touchmove', function (e) {
var t = e.touches[0];
moveSpotlight(t.clientX, t.clientY);
}, { passive: true });
panel.addEventListener('touchend', function () {
panel.style.setProperty('--csr-x', '-200px');
panel.style.setProperty('--csr-y', '-200px');
});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.
What's included
Features
About this UI Snippet
Cursor Spotlight Reveal — A CSS Mask That Follows the Cursor

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
- 1Paste HTML, CSS, and JSA dark panel renders with dim, barely-legible text.
- 2Move the cursor over the panelA circular spotlight reveals vivid text wherever it goes.
- 3Move the cursor awayThe spotlight fades off-panel and the text dims again.
- 4Drag a finger on touchThe same spotlight follows a finger instead of a mouse.
- 5Resize the spotlightChange the circle radius in the mask-image gradient.
- 6Swap the contentReplace the duplicated text with any bright/dim pair.
Real-world uses
Common Use Cases
--csr-x and --csr-y feeding a positioned radial gradient.mask-image and a mousemove handler combine, which is useful for highlighting the exact area a user is pointing at in a dark dashboard.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.