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



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
mousemoveregardless of the system preference. Gate listener registration behindwindow.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-contentremain clickable and reachable via Tab. The cursor is hidden throughcursor: noneon the zone — Tab navigation works, but the focus indicator disappears. Define an explicit:focus-visiblestyle 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
mousemoveflows 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.
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):
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| 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
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.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..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.