webdevpuneet.com

Cursor Spotlight Text Fill — Mask-Image Reveal JS

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

CategoryAnimations

Copy & paste

Source Code

<div class="scene">
  <p class="sub">Move your mouse across the headline</p>
  <h1 class="spotlight-heading" data-text="Light follows every letter">Light follows every letter</h1>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

Two-layer text: a flat muted base plus a masked gradient ::after copy
mask-image radial-gradient centered on CSS custom properties --x/--y
pointermove converts viewport coordinates to percentages via getBoundingClientRect
Zero-cost updates — only custom properties change, not the DOM structure
Automatic idle sweep animation before the first user interaction
requestAnimationFrame-driven lissajous path for the idle sweep
Idle sweep self-cancels on first real pointer movement via { once: true }
Both -webkit-mask-image and mask-image included for Safari support
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Cursor Spotlight Text Fill — mask-image radial-gradient Tracking CSS Custom Properties

Screenshot of the Cursor Spotlight Text Fill snippet rendered live

A cursor spotlight text fill shows a headline in a muted flat color by default, with a small circle of full gradient color that follows the mouse across the letters — as if the reader were holding a flashlight over dim text. It is a more restrained cousin of animated gradient text: instead of the whole headline shimmering continuously, only the area under the cursor lights up, which draws attention exactly where the user is looking rather than everywhere at once. Pair it with cursor spotlight reveal for a matching background treatment.

Two overlapping copies of the same text

The trick relies on content: attr(data-text) inside a ::after pseudo-element that sits exactly on top of the real heading via position: absolute; inset: 0. The base <h1> text renders in a flat muted grey (color: #2a2a38). The ::after copy renders in a gradient using the same background-clip: text technique as animated gradient text — but crucially, that gradient copy is masked so only a small circular region around the cursor is actually visible.

The radial-gradient mask that follows the cursor

mask-image: radial-gradient(circle 140px at var(--x, 50%) var(--y, 50%), #000 0%, #000 30%, transparent 75%) defines a circular mask centered at CSS custom properties --x and --y. Where the mask is opaque black, the gradient-colored ::after text shows through; where it fades to transparent, the muted base heading shows instead. Because radial-gradient's center accepts any length or percentage — including a custom property — updating --x/--y in JavaScript moves the visible "hole" in real time without touching any other CSS.

Updating custom properties from pointermove

moveSpotlight(clientX, clientY) converts the pointer's viewport coordinates into a percentage relative to the heading's own getBoundingClientRect(), then calls heading.style.setProperty('--x', x + '%') and the same for --y. Because the mask-image reads these custom properties directly, no re-render of the mask string is needed — the browser just recomputes the radial-gradient's center on every pointer move, which is cheap because masking is GPU-composited.

The idle sweep before the first interaction

Before any pointermove event has fired, the spotlight would otherwise sit static at 50%/50%. idleSweep() runs a requestAnimationFrame loop that walks --x/--y through a lissajous-style path using Math.sin/Math.cos at different frequencies, so the spotlight glides across the headline on its own to hint at the effect. The loop cancels itself the first time the user actually moves the pointer, handing control over to moveSpotlight.

Step by step

How to Use

  1. 1
    Move the mouse over the headlineA small circle of gradient color follows the cursor across the letters. Before moving the mouse, an idle sweep animates the spotlight automatically.
  2. 2
    Change the headline textUpdate both the visible text inside <h1> and its data-text attribute in the HTML panel — they must match exactly since data-text drives the ::after gradient copy.
  3. 3
    Adjust the spotlight sizeIn the CSS panel, change 140px inside the two radial-gradient() calls (mask-image and -webkit-mask-image) — larger reveals more text at once.
  4. 4
    Change the gradient colorsEdit the linear-gradient stops on .spotlight-heading::after in the CSS panel to match your brand palette.
  5. 5
    Change the base (unlit) text colorUpdate color: #2a2a38 on .spotlight-heading — this is what shows outside the spotlight radius.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Premium and design-forward landing pages
The spotlight-through-text effect signals a high level of craft and is common on design studio, creative agency, and premium SaaS landing pages.
Hero headline that rewards exploration
Encourages visitors to move their mouse across the page, subtly increasing time-on-page and engagement with the hero section.
Learn CSS mask-image with custom properties
Edit --x and --y directly in devtools or the CSS panel to understand how a radial-gradient mask can be driven entirely by custom properties without regenerating any string in JS.
Interactive "reveal the hidden layer" moments
The same two-layer masking technique works for revealing a hidden second color scheme, a secret message, or an easter-egg detail under the cursor.
Pair with animated gradient text
Combine with animated gradient text by also animating the gradient's background-position, so the spotlight reveals a shifting rather than static gradient.
Portfolio hero with a signature interaction
A distinctive cursor-driven text effect helps a personal portfolio hero stand out and demonstrates comfort with modern CSS masking techniques.

Got questions?

Frequently Asked Questions

A ::after pseudo-element duplicates the heading text in a gradient color and sits exactly on top of the base heading via position: absolute; inset: 0. A mask-image radial-gradient centered on the cursor position makes only a small circular region of that gradient copy opaque — everywhere else it is fully transparent, letting the flat-colored base heading show through instead.

radial-gradient() accepts any valid length for its center, including var(--x) and var(--y). Updating those two custom properties with element.style.setProperty() on every pointermove is far cheaper than reconstructing and reassigning the entire mask-image string every frame, and the browser only needs to recompute the gradient center.

Without any pointer interaction, --x and --y would sit at their default 50%/50%, leaving the spotlight static and easy to miss on page load. A requestAnimationFrame loop drives the spotlight through a slow, automatic sine/cosine path until the user actually moves their mouse, at which point the loop cancels itself.

pointermove fires for touch drags but not for simple taps, so on mobile the idle sweep is the primary way the effect is seen. Consider adding a touchmove listener that mirrors moveSpotlight for a touch-and-drag variant.

The base <h1> renders its normal text content in the flat color. The gradient copy comes from content: attr(data-text) on the ::after pseudo-element, which reads a separate data-text attribute rather than the element's own text — the two must be kept in sync manually when you edit the headline.

Yes. Click "JSX" for a React component. Use a ref on the heading, update its style custom properties directly in a pointermove handler via ref.current.style.setProperty(), and drive the idle sweep from a useEffect-managed requestAnimationFrame loop that you cancel on unmount.