# Cursor Spotlight Mask

> 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.

Canonical: https://gsapvault.com/effects/cursor-spotlight-mask
Live demo: https://gsapvault.com/demos/cursor-spotlight-mask/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | mask-reveal, mouse-follow, velocity, hover-effect, parallax |
| Uses Lenis | No |

## Overview

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.

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.

## Features

- 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 gsap.quickTo, 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

## 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

The beneath layer carries a single radial-gradient mask with a fixed opaque stop, and the script only ever writes mask-position and mask-size (plus their -webkit- prefixed twins) as style strings, so the browser reuses one mask image and never re-parses a gradient. Aperture position is eased with gsap.quickTo onto a proxy object, and the per-frame velocity is measured from that eased centre rather than the raw pointer, which is what lets a fast whip build a stretch.

Velocity feeds three derived channels through clamped lerps: a radius boost, and per-axis stretch factors that scale the two mask-size axes while mildly squashing the other. When the aperture stops, a single gsap.to with an elastic.out ease returns all three to zero with overshoot; a second, slower pair of quickTo drivers moves the accent rim so it trails the centre.

gsap.matchMedia splits fine-pointer tracking from a coarse-pointer branch, where a Lissajous drift path and a tap-driven elastic tween replace the cursor, while the reduced-motion branch drops the masking entirely so both compositions sit in normal flow.

## Integration Preview

How this effect integrates into a page. The full documentation (examples, events, programmatic API, customization guide) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

```html
<script data-cfasync="false">document.documentElement.classList.add('has-js')</script>
<link rel="stylesheet" href="path/to/style.css">
```

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:**

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="path/to/script.js"></script>
```

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

```html
<div class="csm-block">
  <div class="csm-stage" data-spotlight-mask>

    <!-- Visible layer. It sits in normal flow and sets the height. -->
    <div class="csm-layer csm-surface">
      <div class="csm-inner">
        <span class="csm-kicker">The case study</span>
        <h3 class="csm-head">
          <span>Holistic</span>
          <span>Brand</span>
          <span>Synergy</span>
        </h3>
        <p class="csm-body">A data-informed framework that unlocks stakeholder value.</p>
      </div>
    </div>

    <!-- Hidden layer. Same structure, opposite words. -->
    <div class="csm-layer csm-beneath">
      <div class="csm-inner">
        <span class="csm-kicker">The group chat</span>
        <h3 class="csm-head">
          <span>We just</span>
          <span>Made it</span>
          <span>Pretty</span>
        </h3>
        <p class="csm-body">Two good ideas, one long night, and a typeface we adore.</p>
      </div>
    </div>

    <span class="csm-rim" aria-hidden="true"></span>
  </div>

  <div class="csm-controls">
    <button type="button" class="btn btn--filled csm-toggle"
            data-spotlight-toggle
            data-label-on="Hide it again"
            aria-pressed="false">Reveal everything</button>
  </div>
</div>
```

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.

### 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. |

### 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`.

### Dependencies

**Required:**

- GSAP 3.14.2 (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.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Cursor Spotlight Mask](https://gsapvault.com/effects/cursor-spotlight-mask)
- [Effects & Templates Vault (£39 one-time, best value): every current and future effect and template](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Hover Underline](https://gsapvault.com/effects/hover-underline.md): Animated link underlines with three GSAP variants: an exit-through slide, a marker-style fill sweep, and a hand-drawn SVG wave. One data attribute per link.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
