Cursors✨ Premium

Cursor Spotlight

A veil at 97% opacity covers the entire zone; on every mousemove, the JS computes the cursor's pixel coordinates inside the container and rewrites them directly into the overlay's background — only a 90 px circle around the pointer stays transparent.

CSSRadial GradientCustom Properties

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

Product reveal section — landing page — Cursor Spotlight example 1

① Product reveal section — landing page

WhenA landing page wants to keep key figures or a striking visual hidden until the visitor actively explores them.
WhyThe visitor is an actor, not a spectator: the torch metaphor creates a micro-narrative and boosts memorability. The effect is void on mobile — never hide conversion-critical information behind this overlay.
SettingsJS radius raised to 120 px for quick passes; opacity lowered to 0.85 if you want the surrounding content to remain vaguely guessable at the edges; overlay color adapted to your brand palette (black, dark indigo, sepia).
Clue hunt — interactive game or onboarding flow — Cursor Spotlight example 2

② Clue hunt — interactive game or onboarding flow

WhenAn onboarding sequence or game wants to make instructions discoverable through physical exploration of the page, not through a 'Show answer' button.
WhyThe content lives in the DOM — readable by search engines and screen readers — but is visually invisible without active interaction. The <code>data-spotlight</code> attribute lets you place multiple independent game zones on the same page without touching the JavaScript.
SettingsJS radius reduced to 50–60 px to make discovery demanding; overlay color changed in all three occurrences (initial CSS + two JS template literals) to prevent an incoherent color flash on mouseleave.
Gallery with hidden captions — artist or photography site — Cursor Spotlight example 3

③ Gallery with hidden captions — artist or photography site

WhenA full-screen gallery overlays title and description on each piece without wanting them permanently covering the image.
WhyThe overlay hides the caption by default and reveals it as the cursor moves — no fade or slide needed. Every zone carrying <code>data-spotlight</code> is wired up automatically: adding an image to the gallery requires no JavaScript change.
SettingsReplace .spotlight-hidden-content with the caption; test rgba(255,255,255,0.92) on a light background; remove cursor: none if you want to keep the native cursor on clickable images.

How it works

The effect relies on a .spotlight-overlay element positioned at inset: 0 above the content. In CSS, this overlay starts with background: radial-gradient(circle 80px at var(--mouse-x, 50%) var(--mouse-y, 50%), transparent 0%, rgba(10,10,26,0.97) 100%). The figure 0.97 is the key: at 97% opacity the background is nearly black and only the gradient's center lets light through. At rest, that hole sits at 50% 50% — a small central window stays open without any interaction, while everything else is hidden.

The mousemove listener is attached to .alchemy-spotlight-zone, the container that holds both the hidden content and the overlay. On every event, getBoundingClientRect() reads the container's position in the viewport; the script subtracts the origin to get absolute pixel coordinates: r = a.clientX - n.left and l = a.clientY - n.top. These values are injected directly into t.style.background via a template literal: radial-gradient(circle 90px at ${r}px ${l}px, transparent 0%, rgba(10,10,26,0.97) 100%). The circle grows to 90 px during movement — slightly larger than at rest — and the JS rewrites the entire background inline on every event: no intermediate CSS variable, the gradient is built and written in one pass.

A second mouseleave listener resets style.background to radial-gradient(circle 80px at 50% 50%, transparent 0%, rgba(10,10,26,0.97) 100%): the opening snaps back to center with no easing. That abrupt return heightens the curtain-drawn feeling — it is a deliberate choice. Activation goes through querySelectorAll('[data-spotlight]'): every zone carrying the attribute is wired up in a single loop, each overlay reacting exclusively to its own zone's events.

Accessibility

  • prefers-reduced-motion: absent from the code. The gradient is rewritten on every mousemove regardless of the system preference. Gate listener registration behind window.matchMedia('(prefers-reduced-motion: reduce)') — but then leave a fixed visible opening, otherwise the hidden content becomes completely inaccessible in reduced-motion mode.
  • The overlay carries pointer-events: none: links and buttons inside .spotlight-hidden-content remain clickable and reachable via Tab. The cursor is hidden through cursor: none on the zone — Tab navigation works, but the focus indicator disappears. Define an explicit :focus-visible style on every interactive element behind the overlay.
  • Outside the 90 px circle, the 97%-opacity layer brings the contrast ratio close to zero — intentionally. If the masked content is not purely decorative, provide an alternative: a Reveal button, or a default-visible version that you replace with the overlay only when JavaScript is available and a fine pointer is detected.
  • On mobile and tablet, no mousemove flows in normal use: the overlay holds its rest position and the 80 px hole stays centered. Content outside that circle is invisible and unreachable by touch. Reserve this effect for decorative content, or expose the content by default and scope the overlay to (hover: hover) and (pointer: fine).

Browser compatibility

The code uses querySelectorAll, arrow functions and template literals — ES2015. IE 11 throws a syntax error before any execution. On the CSS side, radial-gradient(circle Xpx at Ypx Zpx, …) is supported without prefix in all current browsers.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 14+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Frozen overlay (no cursor)
Android Chrome✓ Frozen overlay (no cursor)

Without JavaScript, the overlay keeps its initial CSS <code>background</code>: an 80 px circle centered at <code>50% 50%</code>. Content outside that circle stays hidden — not a full black screen, but not explorable either. For a clean fallback, add the <code>js-ready</code> class at load time and scope the overlay to that selector: without JS, no overlay, the content renders freely.

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
Circle radius during movement (90 in the JS) 90 px The value 90px is in the mousemove template literal. Raise it for a wide beam, lower it for a narrow torch. Do not confuse it with the CSS's 80 px: during movement, 90 applies — the hole is slightly larger than at rest.
Circle radius at rest (80 in the CSS and in mouseleave) 80 px This figure appears in the initial CSS background and in the mouseleave handler. Edit them together: changing them separately makes the circle jump between sizes when the cursor leaves — a visible jolt.
Overlay opacity (0.97 in rgba) 0.97 At 0.97 the background is nearly black. Lower to 0.80 for a light fog that lets the content show through faintly; raise to 1.0 for a fully opaque mask. Update all three occurrences — initial CSS, mousemove and mouseleave — to stay consistent.
Overlay color (10, 10, 26 in rgba) rgba(10, 10, 26, …) The triplet 10, 10, 26 produces a near-black midnight blue. For a white background, switch to rgba(255,255,255,…). Change all three occurrences (CSS + two spots in JS) so the curtain blends into the page without a visible seam.
Trigger attribute (data-spotlight) [data-spotlight] querySelectorAll('[data-spotlight]') wires up every zone carrying the attribute, with no limit. For per-zone configuration, read el.dataset.spotlightRadius inside the loop and use that value in the template literal — configuration stays in HTML.
Hidden native cursor (cursor: none on .alchemy-spotlight-zone) native cursor hidden Hiding the cursor reinforces the illusion that the beam is the pointer itself. Remove cursor: none if you prefer the native cursor — the effect works without it, but you then see two circles moving together (the pointer and the halo), which reduces immersion.

FAQ

The two share a similar name but use opposite mechanics. Spotlight Reveal applies a CSS mask (mask: radial-gradient) to a reveal element — content is visible only where the mask is opaque — and the JS writes coordinates as CSS variable percentages (--x: 45%). Cursor Spotlight does the reverse: an opaque overlay covers everything and the JS punches a hole by rewriting the entire background in pixels (circle 90px at 312px 87px) on every mousemove, with no intermediate variable. In practice, Spotlight Reveal makes colored text appear on a dark background; Cursor Spotlight can hide any content behind a real curtain and activate it on as many zones as needed through data-spotlight.
The radius is hard-coded in the JS (90 px during movement). To make it configurable per zone, store it in the HTML: data-spotlight-radius="120", then read parseInt(el.dataset.spotlightRadius, 10) || 90 inside the querySelectorAll loop and use that variable in the mousemove template literal. The existing loop stays unchanged — configuration lives in HTML, not JavaScript.
The overlay obscures without removing: .spotlight-hidden-content stays in the normal flow, readable by search engines and screen readers, and clickable as long as pointer-events: none is correctly set on the overlay. This is not an SEO concealment technique — bots see the text. If you truly need to hide content from search engines, load it via JavaScript only after an explicit user action.