Cursors✨ Premium

Spotlight Reveal

Content hidden in a dark zone, revealed under the cursor by a radial CSS mask: two percentage-based variables simultaneously drive the visibility window and the colored glow that illuminates what it lets through.

JSReveal

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →

3 usage examples

Pre-launch teaser zone — coming soon page — Spotlight Reveal example 1

① Pre-launch teaser zone — coming soon page

WhenA pre-launch page shows a dark background and an enigmatic phrase. The visitor is invited to explore with their mouse to discover what is hidden.
WhyThe mask progressively reveals the promise as the visitor moves around. The absence of <code>mouseleave</code> works in your favor: already-discovered fragments stay readable, revelations accumulate. Narrative tension builds without a dedicated JavaScript layer.
SettingsMask window at 150 px for longer text; glow reduced to 40 px, opacity at 0.12 so the glow does not overpower the text.
Hidden promo code or bonus — pricing or loyalty section — Spotlight Reveal example 2

② Hidden promo code or bonus — pricing or loyalty section

WhenA 'Secret perk' block invites the subscriber to hover to discover a discount code. The content is in the DOM but invisible until the cursor passes over it.
WhyThe reveal is instant on hover, no popup. Since the text is in the DOM, the visitor can select and copy it once visible — no extra click. Curiosity is activated without complex JavaScript.
SettingsMask radius at 120 px to reveal everything at once; glow color matching your brand; add a :focus-visible state on the zone for keyboard users.
Hidden titles over a thumbnail grid — portfolio or catalog — Spotlight Reveal example 3

③ Hidden titles over a thumbnail grid — portfolio or catalog

WhenA grid of images with no visible caption: each thumbnail carries a title in overlay that the visitor discovers by exploring with their mouse.
WhyApplied to each cell via the multi-instance pattern described in the FAQ, the effect turns the grid into an exploration space. Percentage-based coordinates adapt automatically to any cell size — one configuration for the whole grid.
SettingsMask radius at 80–90 px on 200–250 px thumbnails; bold high-contrast text to stay readable through the indigo glow.

How it works

The effect relies on two layers inside the .spotlight-zone container. At the bottom, the ::before pseudo-element renders a ghost text at 10% opacity. On top, the .spotlight-reveal div covers the entire zone through inset: 0: it does not hide content, it is the content — the revealed text lives directly inside it. This div receives two radial-gradients simultaneously, both driven by the same --x and --y variables: a background that casts an indigo glow (rgba(99, 102, 241, .3), 80 px radius) and a mask that cuts a circular window of 100 px. The mask does all the work: where it is black, the element is visible; where it is transparent, it disappears.

The mousemove listener is attached to #zone-spotlight, not to .spotlight-reveal. On every move, getBoundingClientRect() reads the zone's geometry, and the script computes coordinates as percentages: x = ((e.clientX − rect.left) / rect.width) * 100, same for Y. These values are written into --x and --y via style.setProperty(). The browser instantly recomputes both gradients — window and glow move together in a single call. No mouseleave is declared: when the cursor leaves the zone, the variables stay frozen.

Choosing a CSS mask — rather than an opaque overlay — changes the reading: the content layer opens up, not a lid lifted. What falls outside the circle does not render but stays in the DOM — the hidden text remains accessible to assistive technologies. The two radii — 80 px for the glow, 100 px for the window — are independent. Their 20 px gap creates a fringe where the mask fades before the color reaches zero: the illumination edge appears blended, not sharply cut.

Accessibility

  • prefers-reduced-motion: absent from the code. Gate the listener registration behind window.matchMedia('(prefers-reduced-motion: reduce)') before deploying — in reduced-motion mode, leave the variables at their 50% 50% fallback for a centered, static mask.
  • On touch screens, no mousemove fires: the variables stay at their default values, the window stays centered, and the content is statically visible there — a sound fallback, but not interactive. Never make a critical action depend on hover if part of your audience has no mouse.
  • Without mouseleave, the spotlight stays frozen at the last position when the cursor exits. If the masked content is functional, add a mouseleave listener that resets --x and --y to 50%.
  • The content of .spotlight-reveal is in the DOM — the mask does not touch the accessibility tree. But the ::before ghost text and the revealed text coexist for assistive technologies: if the ::before carries an instruction, mark it aria-hidden= rue\.

Browser compatibility

The unprefixed mask property is supported in Chrome 120+, Firefox 53+, and Edge 79+. For Safari, -webkit-mask is required up to 15.3; from 15.4 the unprefixed alias works. The rest — CSS variables, radial-gradient, inset: 0 — poses no issue. The JavaScript uses arrow functions (ES2015): IE 11 stops before the first event.

Chrome 120+✓ Full
Firefox 53+✓ Full
Safari 15.4+✓ Full (add -webkit-mask for 9.1–15.3)
Edge 79+✓ Full
Mobile iOS✓ Static centered mask (no cursor)
Android Chrome✓ Static centered mask (no cursor)

Without JavaScript, the variables stay at their <code>50% 50%</code> fallback: the window remains centered and static, the content is visible — a sound fallback, not a blank screen. To cover Safari before 15.4, duplicate both mask properties with the <code>-webkit-mask</code> prefix before <code>mask</code>. No other dependency.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Mask radius (100px in the CSS) 100 px The mask: radial-gradient(circle 100px at …) property controls the window size. Increase it to reveal more at once; shrink it for a narrow window. This radius is independent of the glow — keep them different to preserve the blending fringe.
Glow radius (80px in the background) 80 px Drives only the colored light patch. Smaller than the mask radius, it creates a lit area that fades before the window edge — the flashlight effect. Match both radii for a sharp, colored edge.
Glow color and opacity (rgba(99, 102, 241, .3)) indigo at 30% Change all four RGBA values to adapt hue and intensity. 0.1 = near-invisible luminescence; 0.7 = dominant glow that strongly tints the content. On a light background, a dark glow gives an inverted lens effect.
Revealed content (text node inside .spotlight-reveal) plain text Replace the text node with any HTML: image, SVG, badge, link. The mask applies to everything inside .spotlight-reveal. Avoid form controls — their click area follows the mask's visible edge, which can surprise users.
Ghost text (::before on .spotlight-zone) "Déplacez la souris ici" at 10% opacity Change the content value for a different prompt, lower opacity to 0.05, or remove it if the zone is self-explanatory. This pseudo-element is separate from .spotlight-reveal and unaffected by the mask.
Cursor-exit behavior (no mouseleave listener) frozen at last position Without mouseleave, the spotlight stays where the cursor last stopped. Add a listener that resets --x and --y to 50% to re-center on exit. Or set both to -200% to push the mask out of the zone and hide all content.

FAQ

The difference is architectural. Here, the content layer carries the CSS mask: the window opens inside the element holding the text, what falls outside the circle disappears. In 'Cursor Spotlight' (fx-0214), an opaque overlay covers the background with a transparent hole at the cursor — the content sits below, the lid lifts. Three code differences on top: fx-0201 computes in percentages, fx-0214 in absolute pixels; fx-0201 has no mouseleave, fx-0214 recenters on exit; fx-0214 hides the system cursor (cursor: none), fx-0201 leaves it visible.
The code targets two ids via getElementById — a single pair. For multiple instances, switch to querySelectorAll('.spotlight-zone') and loop, retrieving each child .spotlight-reveal and attaching the listener with local references in the closure. Since the variables are written to each element's inline style, instances are fully independent.
Percentages plug directly into radial-gradient(), which interprets them as a ratio of the element's dimensions. If the zone is resized, the next mousemove re-reads getBoundingClientRect() and corrects positioning automatically — no ResizeObserver needed, unlike a pixel-based approach.