Cursors✨ Premium

Spotlight Cursor

A 300 px indigo halo — a radial-gradient at 40% opacity fading out at 70% — drawn by a ::before centered on the pointer in pixels, invisible at rest and faded in over 300 ms on hover. The native cursor stays: this is ambient lighting that follows the hand, not a replacement cursor.

JSGradient

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

Footer band — “Ready to start?” — Spotlight Cursor example 1

① Footer band — “Ready to start?”

WhenLast section of a landing page: title, one sentence, a button. Plain black background.
WhyA flat black background at the bottom of the page is dead space; the halo following the mouse gives it depth right when the user hesitates, without competing with the button, which stays above the halo.
Settings300 px halo and width: 100% on the container (defaults), text and button at z-index: 1.
Feature card — halo contained in the card — Spotlight Cursor example 2

② Feature card — halo contained in the card

WhenProduct feature grid: a card with icon, title, description.
WhyThe container's overflow: hidden clips the halo to the card's rounded edges: the light seems to come from inside the component, a “glass” look without backdrop-filter.
Settings360 px halo in rgba(20,184,166,.35) (product hue), container with border-radius: 18px, .4s transition.
Technical hero — halo over a background grid — Spotlight Cursor example 3

③ Technical hero — halo over a background grid

WhenHeader of a developer tool: fine grid background, monospace title, two links.
WhyOver a grid, the halo reveals the lines it crosses like a lamp over a blueprint: the effect reads the background's structure instead of hiding it.
Settings420 px halo, mix-blend-mode: screen on the ::before, .5s transition, gradient stop at 60% for a shorter edge.

How it works

The #zone-spotlight-container container (.spotlight-container) is position: relative, #0a0a0f background, overflow: hidden. Its ::before carries the whole effect: 300 × 300 px, background: radial-gradient(circle, rgba(99,102,241,.4) 0, transparent 70%), left: var(--cursor-x, 50%), top: var(--cursor-y, 50%), transform: translate(-50%, -50%) to center it on that point, pointer-events: none, opacity: 0 and transition: opacity .3s. The .spotlight-text is position: relative; z-index: 1 to stay above the halo.

The JS is a single mousemove: x = clientX − rect.left, y = clientY − rect.top, then setProperty('--cursor-x', x + 'px') and '--cursor-y' on the container. Pixels relative to the container (measured: --cursor-x: 77.7px, --cursor-y: 120px for a pointer at those coordinates). No requestAnimationFrame, no smoothing: the halo is glued to the pointer, and nothing runs outside hover (0 rAF calls/s measured). Each event moves a pseudo-element's left/top — a relayout limited to it.

Appearance is pure CSS: .spotlight-container:hover::before { opacity: 1 }, a 300 ms fade on entry and on exit. The variables are never reset (measured: still 77.7px after leaving): on the next entry the halo lights up where it was until the first movement. The gradient reaches transparent at 70% of the 150 px radius, about 105 px: the visible disc is roughly 210 px across, clipped by the container's overflow: hidden at its edges. At the core, rgba(99,102,241,.4) over #0a0a0f composites to #2e2f69 — 1.6:1 against the background, a glow, not a lamp.

Two integration facts. The .cursor-zone rule sets width: 100%: the zone fills its parent and the 300 px halo has room to spread — without that width, inside a centered flex parent, the zone would shrink to its text's width (261 px measured in a 760 px preview). No cursor: none: the system arrow remains visible.

Accessibility

  • Text contrast: rgba(255,255,255,.7) on #0a0a0f = 9.8:1, and 6.8:1 even at the halo's core (composited background #2e2f69) — readable everywhere. The .info-text at rgba(255,255,255,.5) = 5.3:1 for 14 px, compliant.
  • The halo itself measures 1.6:1 against the background: it is an ornament, not a component, and must carry no information (WCAG 1.4.11 doesn't apply as long as it stays decorative). Don't make it the only cue of an interactive area.
  • prefers-reduced-motion: the only autonomous motion is the 300 ms fade; the halo follows the hand, it isn't animated. Optional but clean: @media (prefers-reduced-motion: reduce) { .spotlight-container::before { transition: none } }.
  • Touch and keyboard: on a touch screen, a tap emulates mousemove and :hover — the halo lights up at the tapped point and stays until the next tap. On keyboard, nothing happens. Acceptable since the effect is decorative; if it underlines links, keep an explicit :focus-visible on them.
  • Screen readers: the ::before has content: "", nothing is announced. If you place links inside the container, give them position: relative; z-index: 1 like .spotlight-text so they stay above the halo, and check that overflow: hidden doesn't clip their focus ring near the edges.

Browser compatibility

Requires CSS custom properties with setProperty, a positioned and transformed ::before, and radial-gradient(). ES2015 JS (arrow functions, const). Zero dependencies.

Chrome 49+✓ Full
Firefox 45+✓ Full
Safari 10+✓ Full
Edge 15+✓ Full
Mobile iOS✓ Halo frozen at the tap point
Android Chrome✓ Halo frozen at the tap point

Without JavaScript, the halo stays centered (50%/50% fallback) and still lights up on hover: a consistent static vignette. Without CSS variables (pre-2016 browsers), left/top become invalid and the pseudo-element lands at the container's top-left.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="cursor-zone spotlight-container" id="zone-spotlight-container">
  <span class="spotlight-text">Move the cursor to illuminate</span>
  <span class="info-text" style="color: rgba(255,255,255,0.5);">CSS spotlight effect</span>
</div>
🔒 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
Halo size (CSS — ::before width/height) 300 px Diameter of the gradient box; the visible disc is 70% of it (≈ 210 px). The translate(-50%, -50%) keeps the center under the pointer whatever the size.
Color and intensity (CSS — rgba(99,102,241,.4)) indigo at 40% Hue and strength of the glow at the center. .6 becomes a lamp; a warm color (rgba(251,191,36,.35)) gives a stage spotlight.
Gradient fade (CSS — transparent 70%) 70% Radius at which the halo dies out. 50% = compact soft-edged disc; 100% = glow filling the whole 300 px box.
Transition (CSS — ::before transition) opacity .3s Duration of the light-up/dim on hover. Add left .15s, top .15s for a halo that follows with a slight lag — pixels interpolate, unlike untyped percentages.
Fallback position (CSS — var(--cursor-x, 50%)) 50% / 50% Where the halo sits before the first movement and without JS. 0% tucks it in a corner; the value is used until the JS writes the variable.
Blend mode (CSS — ::before, to add) normal mix-blend-mode: screen brightens what it hovers (images, grids) instead of laying a colored layer; plus-lighter for a stronger additive glow.
Container width (CSS — .cursor-zone width) 100% The zone takes its parent's full width. A fixed width (480px) confines the halo to a card; avoid auto inside a centered flex parent — the zone shrinks to its text's width (261 px measured out of 760) and overflow: hidden clips the 300 px halo on every side.

FAQ

The shipped .cursor-zone rule sets width: 100%: the zone fills its parent. If it shrinks to its text's width, that declaration was removed or overridden by a more specific rule — inside a centered flex parent, the zone then measures 261 px out of 760 px available, and overflow: hidden cuts the 300 px halo at the edges. You can also apply the effect to a block that already has its size (section, card). The three conditions to keep: position: relative, overflow: hidden and a height.
Yes, with no extra JavaScript: the variables are written in pixels, so left and top receive interpolable lengths. Add left .15s ease-out, top .15s ease-out to the ::before's transition; the halo then glides to each new position. For a more physical smoothing (per-frame factor), reuse Dot Follower's requestAnimationFrame loop (fx-0197).
The JS only handles #zone-spotlight-container, by id. Replace getElementById with document.querySelectorAll('.spotlight-container').forEach(zone => { … }), attaching the listener to each zone: the CSS, already written on the class, applies to every instance. Each container writes its own variables, there is no collision.