Cursor Spotlight Mask

£5

A soft aperture follows the cursor and reveals a completely different layer hidden under the visible one, stretching into a lens streak on the axis of movement and springing back with an elastic settle.

advanced
5 more details
mask-revealmouse-followvelocityhover-effectparallax
Cursor Spotlight Mask - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

A cursor-driven reveal effect that stacks two complete compositions in one box and shows the hidden one only through a soft, moving aperture. The surface layer says one thing, the layer beneath it says the opposite, so the reveal reads as a hidden truth rather than a highlighter.

Read the full effect overview

The aperture is not a fixed circle. One input drives four visible consequences at once: it trails the pointer with an eased lag, grows with pointer speed, stretches along the axis of movement so a fast horizontal whip draws it into a lens streak, and drags a thin accent rim a beat behind its centre while the revealed layer parallaxes against the surface.

Everything springs back to the rest circle on an elastic ease, overshooting rather than snapping. On touch devices the aperture wanders on its own slow path and a tap springs it to the tapped point, and a real Reveal everything button opens the aperture over the whole composition for keyboard users and anyone who would rather just read both.

What's included

8 items
  • Two stacked compositions revealed through one moving aperture, driven entirely by mask-position and mask-size so the gradient is never rebuilt
  • Velocity-reactive aperture: radius grows with pointer speed and stretches on the axis of movement, with a counter-squash that makes a whip read as a lens rather than a bigger circle
  • Elastic settle (elastic.out) that overshoots the rest circle and springs back, never a linear snap
  • Accent rim on its own slower follow, so it smears behind the aperture and lands last
  • Parallax on the revealed layer so the two compositions never feel like one flat image
  • Touch answer: the aperture drifts along a slow Lissajous path and a tap springs it to that point with an elastic settle
  • Real Reveal everything button with maintained aria-pressed that opens the aperture over the whole composition
  • GSAP core only with no plugins, every value set from data attributes, and a graceful floor: no JavaScript, no mask support, or reduced motion leaves both compositions as stacked, fully readable blocks

Perfect for

5 use cases
  • Agency and portfolio hero sections with a claim on the surface and the honest version underneath
  • Product pages revealing a before and after, or a spec sheet hidden under a lifestyle shot
  • Editorial and campaign pages where the interaction itself carries the joke or the reveal
  • Pricing or comparison blocks that expose the small print through the aperture
  • Case study cards on studio sites that reward the visitor for moving the cursor

How it works

3 sections

The layer beneath carries a single radial-gradient mask, and the script only ever moves and resizes it rather than rewriting the gradient, so the browser reuses one mask image instead of re-parsing on every frame. The aperture eases toward the cursor, and speed is read from that eased centre rather than the raw pointer, which is what lets a fast whip build a stretch.

Speed feeds three channels: a radius boost and a per-axis stretch that scales one direction while mildly squashing the other. When the aperture stops, all three return to rest with a single overshoot; the accent rim runs on its own slower follow so it trails the centre.

Fine-pointer tracking and touch are separate branches: on touch, a drifting path and a tap-driven pulse replace the cursor. Reduced motion drops the masking entirely so both compositions sit in normal flow.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Paid effect

Purchase to unlock the code.

Buying Cursor Spotlight Mask opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The one-line script is not optional. Everything that depends on JavaScript is gated behind has-js, so if the class is missing (or the effect script never loads) the two layers stay in normal flow as stacked, fully readable blocks instead of one layer sitting invisible on top of the other.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Add the markup anywhere in your <body>:

Code snippet omitted: it ships with the download.

Three structural rules, all of which the effect depends on:

  1. .csm-surface must come first and stay in normal flow. It defines the height of the box; .csm-beneath is lifted on top of it with inset: 0.
  2. Give both layers the same structure. The reveal reads as the same block re-lettered only if the lines land in the same places. Keep headline lines short enough not to wrap, and keep both bodies to a similar length.
  3. .csm-inner is what parallaxes. Content must live inside it, not directly in .csm-layer. The mask lives on .csm-layer, so transforming the layer itself would move the aperture with it.

Using It With Your Own Design

Keep .csm-stage, the two .csm-layer elements, .csm-surface, .csm-beneath and their .csm-inner wrappers. The surface stays in normal flow and sets the stage height; the beneath layer must retain absolute positioning, inset: 0 and its mask inside the .has-js gate. The rim is optional and must keep pointer-events: none.

The Mona Sans typography, yellow reveal, copy grid, captions and comparison rows are demo styling. Replace them with your own content and colours, keeping the two layers' dimensions and text positions aligned. Preserve the no-JavaScript and reduced-motion rules so both compositions remain readable without animation. Keep any reveal button inside the stage's parent .csm-block.

Options

Set these on the .csm-stage element.

Attribute Values Default Description
data-spotlight-mask (presence) required Marks the container. Without it nothing initialises.
data-radius px number, or a percentage 180 Rest radius of the aperture. 26% resolves against the container's shorter edge. Capped at 42% of that edge so narrow screens keep a usable aperture.
data-stretch true, false true Stretch the aperture along the axis of movement. false keeps it circular and lets it grow with speed only.
data-ease seconds 0.42 How far the aperture trails the pointer. The lag is what generates the velocity the stretch reads, so below about 0.15 the aperture sticks to the cursor and barely stretches at all.
data-drift true, false true The autonomous wander: always on for touch devices, and on desktop after 2.5 seconds without cursor movement. false parks the aperture where it was left.
data-rim true, false true The trailing accent ring on the aperture edge.

And on the button:

Attribute Values Default Description
data-spotlight-toggle (presence) optional Marks the open/close control. It is paired with the container by living inside the same parent element.
data-label-on any text Hide it again Button label while the aperture is open. The closed label is whatever text the button ships with.

Customising the aperture edge

The aperture's softness lives in one CSS declaration:

Code snippet omitted: it ships with the download.

82% is where the mask stops being fully opaque, so the hidden layer is solid across almost the whole aperture and only a thin ring blends. Raise it towards 100% for a hard-edged porthole. Dropping it much below 70% widens the blend zone until both layers show at half strength over a large area, and overlapping type there reads as a rendering fault rather than a reveal. If you change it, change OPAQUE_STOP in script.js to match: that constant is how the script works out the radius needed to cover the whole composition when the button is pressed.

The script never touches mask-image. It only writes mask-position and mask-size (and their -webkit- twins), which is why the reveal stays cheap at 60fps: the browser keeps one mask image and re-rasterises its box.

Accessibility

  • Reduced motion: prefers-reduced-motion: reduce drops the masking entirely, in CSS and again in the script's matchMedia branch. Both compositions revert to stacked blocks in normal flow, so every line of copy is legible, and the toggle button hides because there is nothing left to reveal.
  • Keyboard: the Reveal everything button is a real <button> with a maintained aria-pressed state, visible to everyone rather than hidden as an accessibility afterthought. It opens the aperture over the whole composition and closes it again.
  • Assistive tech: the hidden layer is masked, never display: none, visibility: hidden, or aria-hidden, so its text stays in the accessibility tree and in the document for search engines. Both compositions are read in DOM order.
  • Touch: there is no cursor to chase, so the aperture drifts along a slow autonomous path and a tap springs it to the tapped point with an elastic settle. Both layers stay discoverable on a phone.
  • No JavaScript, no mask support: the layers stay in normal flow as two stacked, readable blocks. Nothing is ever left at opacity: 0.

Performance notes

  • The aperture is driven from a single gsap.ticker callback that writes four style properties per container per frame and reads no layout. Container geometry is cached and re-measured only on resize and scroll, both rAF-throttled.
  • will-change: transform is set on .csm-inner and .csm-rim. If you stack many of these on one page, drop it from .csm-inner first.
  • Every listener is removed and every tween killed when the breakpoint changes or the page unloads.

Dependencies

Required:

  • GSAP 3.15.0 (core only)

Not used: ScrollTrigger, Lenis, or any other plugin.

Masking needs mask-image support (all current browsers; Safari is covered by the -webkit- prefixed properties, which ship alongside every write). The script feature-detects it and falls back to the stacked layout where it is missing.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

Everything in the catalogue, plus everything we release next.