Glow Effect
A distinct glowing orb follows the pointer inside the zone: `mouseenter` reveals it via `display: block`, every `mousemove` repositions it with a fixed 40 px offset on each axis, `mouseleave` hides it — and the zone receives `cursor-active` so CSS can erase the native cursor.
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 strictly separates boundary and cursor. #zone-glow defines the stage: dark background, one-rem border-radius, and crucially overflow: hidden — the silent boundary that clips the orb at its edges without extra JS. #cursorGlow is a separate DOM element, outside the zone's flow, display: none by default. Only it moves; the zone never does. This role split sets the effect apart from a radial-gradient background: no layer recomputed each frame — a positioned element tracking the pointer.
Three listeners, three strict roles. mouseenter runs two instructions: add cursor-active to #zone-glow (CSS hook for cursor: none), then flip #cursorGlow to display: block — instant appearance, no fade. mouseleave reverses the pair in the same order. Between the two, mousemove rewrites the position on every event: cursorGlow.style.left = e.clientX - 40 + 'px' and cursorGlow.style.top = e.clientY - 40 + 'px'. Two inline style assignments per move — no requestAnimationFrame, no getBoundingClientRect(). The update is synchronous with every pointer movement.
clientX and clientY measure the pointer position in the viewport, not inside the zone. Subtracting 40 px on each axis centers an 80 × 80 px orb on the cursor: the offset equals half the orb's dimensions, and #cursorGlow must be in position: fixed so that left: clientX − 40px is valid regardless of page scroll. overflow: hidden clips the orb visually at the zone's edges without making it a CSS child of the zone. Change the orb's size and you must update the offsets to width / 2 and height / 2.
Accessibility
- prefers-reduced-motion: no
transitionor@keyframesin the base snippet. Real use of#cursorGlowalmost always involves afilter: blur()or a pulse — gate those behind@media (prefers-reduced-motion: reduce). The repositioning vialeft/topis not CSS animation and ignores motion media queries: to cover sensitive visitors, disable themousemovelistener entirely when the preference is active. - The orb stays
display: noneon touch screens: nomouseenterormousemovefires in normal touch use. If you applycursor: noneinside.cursor-active, be careful: some hybrid browsers fire a synthetic hover on first tap. Gatecursor-activebehindmatchMedia('(hover: hover) and (pointer: fine)')for hybrid environments. - None of the three listeners responds to keyboard events: Tab, Enter, Space work normally. If you use
cursor: noneinside.cursor-active, ensure the rule disappears as soon as the pointer leaves the zone. Define a strong:focus-visiblestate on interactive elements — the glow orb is not a visual substitute for keyboard navigation. #cursorGlowis purely decorative — no text, no role. Addaria-hidden="true"to it: adivflipping fromdisplay: nonetodisplay: blockcan be announced by some AT tools without it. Never carry information through the glow itself — it is invisible to a significant portion of your visitors.
Browser compatibility
The effect uses only basic DOM: getElementById, classList.add/remove, and assignments to style.left/style.top/style.display. No custom properties. The only floor is syntactic: arrow functions require ES2015 — IE 11 halts before the first mouseenter.
Without JavaScript, <code>#cursorGlow</code> stays <code>display: none</code>: the zone renders normally, the native cursor stays visible. On an ES5 engine, the syntax error stops the script before any listener is attached — the same clean fallback. The effect is purely additive.
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 |
|---|---|---|
| Centering offset (40 in the JS) | 40 px — for an 80 × 80 px orb | The value 40 appears twice: e.clientX - 40 and e.clientY - 40. It must equal half the orb's dimensions. A 120 × 60 px orb requires asymmetric offsets (60 and 30). An incorrect offset miscenters the halo and exposes the mechanics. |
| Display toggle (display: none → block) | display: none / display: block | display is binary and cannot be transitioned. For a fade, replace it with opacity + visibility and transition: opacity .2s: visibility: hidden removes the orb from pointer events during invisibility, preventing stray mousemove events if the zone and orb overlap. |
| cursor-active class on #zone-glow | class with no style imposed by the JS | The JS adds and removes cursor-active in mirror with the orb's visibility; your CSS decides what to do. Main use: #zone-glow.cursor-active { cursor: none } to hide the native cursor. Also useful to change the zone's border-color or trigger a CSS animation on the background. |
| Orb identifier (#cursorGlow) | getElementById('cursorGlow') | The orb is fetched by id at startup; absent, the script exits cleanly (if (!cursorGlow) return). Update the id in both JS and HTML. Shape, size, color, and animations all live in your CSS — the JS only knows the id, the display state, and the left/top coordinates. |
| Listening boundary (#zone-glow) | 300 px tall div with overflow: hidden | All three listeners are attached to #zone-glow. Change its dimensions or border-radius: overflow: hidden clips the orb differently, the JS mechanics stay untouched. To cover several zones, share one #cursorGlow — see FAQ. |
| mouseenter and mouseleave presence | both active, strict mirror | Removing mouseleave leaves the orb visible once the pointer has entered the zone — useful when the zone covers most of the viewport. Keeping only mousemove without the other two creates an orb that tracks but never displays: you also need to switch the initial display to block. |
FAQ
clientX / clientY give the viewport position, independent of scroll. pageX includes scroll — the orb would drift down. offsetX varies based on the actual target element: a child of #zone-glow would shift the reference and cause jumps. clientX − 40 stays stable because #cursorGlow in position: fixed is also viewport-relative.display: none → block is binary and cannot be transitioned. For a fade, initialize #cursorGlow with opacity: 0; visibility: hidden; transition: opacity .2s. On mouseenter, set opacity: 1; visibility: visible; on mouseleave, reverse. visibility: hidden preserves the element's dimensions but removes it from pointer events during invisibility, preventing conflicts if the zone and orb overlap.#cursorGlow in the DOM and attach the same listener triplet to each zone: the orb appears and repositions in whichever zone the pointer enters, hides on mouseleave. If zones do not overlap, the transition is natural. If they do, an active-zone counter prevents the first zone's mouseleave from hiding the orb while it has already entered the second.