Cursors✨ Premium

Custom Circle Cursor

A circle substituted for the native cursor inside a delimited zone: `mouseenter` shows it and erases the system pointer, `mousemove` teleports it to the exact pointer coordinate — zero interpolation, zero lag — and checks on every event whether the hovered element carries the `data-hover` attribute to switch the circle's visual state.

JSBlend

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

Dark photo gallery — creative portfolio — Custom Circle Cursor example 1

① Dark photo gallery — creative portfolio

WhenA photo portfolio presents full-bleed visuals on a black background and wants an interaction signature with no tooltip and no overlay.
Why<code>mix-blend-mode: difference</code> on the circle inverts colors under the pointer: light areas of the image turn dark, dark areas turn light. Without adding any extra JS, the circle reveals the image's texture as the visitor moves across it.
SettingsCircle at 80–100 px; difference or exclusion depending on contrast; restrict the zone to the gallery container to keep the native cursor in navigation.
Interactive canvas or visualization panel — web tool — Custom Circle Cursor example 2

② Interactive canvas or visualization panel — web tool

WhenAn editor or internal dashboard wants to signal that the user has entered a working area with different interaction rules from the rest of the interface.
WhyThe custom cursor visually delineates the active zone without adding a border or background color. The <code>hover</code> state change can flag handles or editable elements on approach, without extra tooltips.
SettingsThin circle (2 px border, transparent fill); mix-blend-mode: normal with a distinctive color; add data-hover to resize grips and selectable areas.
Scrollytelling section or interactive hero — editorial site — Custom Circle Cursor example 3

③ Scrollytelling section or interactive hero — editorial site

WhenA longform page uses mouse movement as a narrative vector: the visitor reveals annotations or interacts with an illustration as they move the cursor.
WhyThe circle signals that movement is active and meaningful, without a text instruction. Its <code>hover</code> state changes over annotated zones (carrying <code>data-hover</code>), inviting a click. The effect stays in the editorial zone only.
Settings60 px circle with slight opacity; mix-blend-mode: multiply on a light background for an ink-like render; state changes on data-hover zones can trigger a separate CSS animation — the circle acts as a trigger.

How it works

Three listeners are attached to the #zone-circle container, never to the circle itself. mouseenter fires two actions at once: it adds the cursor-active class to the zone — which activates the CSS rule cursor: none and hides the system pointer — and switches the circle from display: none to display: block. mouseleave reverses all of it: removes cursor-active, resets the circle to display: none and strips hover for a clean state on the next entry.

On every mousemove, the circle receives two values: style.left = e.clientX + 'px' and style.top = e.clientY + 'px'. These are viewport coordinates written directly, with no coefficient, no requestAnimationFrame, no lerp. The circle does not follow the cursor with a delay or a trail — it occupies the exact same position at every instant. Centering at the contact point is handled by transform: translate(-50%, -50%) in CSS. The mix-blend-mode property on the circle blends it visually with the content underneath (inversion, subtraction, multiplication) — no extra JS.

Still inside mousemove, after repositioning, the code inspects e.target: if that element has the data-hover attribute, the hover class is added to the circle; otherwise it is removed. This check runs on every event, with no cache and no delay — the switch is instantaneous. The JS has no knowledge of the circle's appearance: it supplies a binary state that CSS interprets visually. The data-hover attribute is your semantic selector — you designate active elements inside the zone without touching the JS.

Accessibility

  • Hiding the native cursor is a heavy accessibility decision — not just an animation. If the custom circle fails to load (blocked JS, rendering error), cursor: none removes the user's pointing reference without substituting anything. Verify that #cursorCircle is present in the DOM before attaching listeners.
  • On keyboard, mousemove never fires: the user never sees the circle, and if cursor: none applies through another rule, the visual reference disappears without a substitute. Make sure every clickable element inside the zone has an explicit :focus-visible state — a focus ring, a shadow — independent of any cursor effect.
  • On touch screens, no mouse* events flow: the circle stays at display: none, the zone keeps its native cursor. Clean fallback, no extra code. To block emulated mousemove on tap, gate the listeners behind matchMedia('(hover: hover) and (pointer: fine)').
  • The circle is a purely visual ornament. Make sure it carries pointer-events: none in CSS — otherwise it intercepts mouse events at its own position and breaks data-hover detection on the elements underneath. Never carry information through the circle or its hover state alone — part of your audience will never see either.

Browser compatibility

All APIs used are ES5-compatible: getElementById, classList, addEventListener, style.left/top/display, hasAttribute. The only floor is syntactic: arrow functions (ES2015) halt IE 11. On the CSS side, mix-blend-mode is the limiting factor — absent from IE and pre-Chromium Edge, where the circle renders without blending.

Chrome 79+✓ Full
Firefox 72+✓ Full
Safari 13.1+✓ Full
Edge 79+✓ Full
Mobile iOS✓ Inert (touch)
Android Chrome✓ Inert (touch)

Without JavaScript, the circle is never made visible and <code>cursor-active</code> is never added: the native pointer stays intact. Without <code>mix-blend-mode</code> (Edge 18 and earlier), the circle renders as a solid flat shape — a functional fallback if you assign it a readable background color. No fallback code to write in either case.

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
Activation zone (#zone-circle) the 300 px tall container in the demo All three listeners (mouseenter, mouseleave, mousemove) are attached to this element. Replace the id with any container or document.body for a full-page cursor.
Circle size (CSS width/height on #cursorCircle) defined in CSS, not hard-coded in the JS The JavaScript only writes left and top — it has no knowledge of the circle's size. Change width and height freely in CSS, centering remains handled by transform: translate(-50%, -50%).
mix-blend-mode (CSS on #cursorCircle) defined in CSS — 'difference' in the demo difference inverts colors under the circle. exclusion gives a softer similar result. multiply darkens (light backgrounds). screen lightens (dark backgrounds). normal disables blending for a solid flat shape — preferable in work interfaces.
The data-hover attribute present on the demo button (data-hover="true") On every mousemove, the code tests e.target.hasAttribute('data-hover') — presence only, value ignored. Place it on any HTML element (link, image, SVG, button) to trigger hover on the circle.
The .hover class on #cursorCircle (CSS) toggled by data-hover — appearance entirely defined in CSS The JS adds or removes .hover; all visual rendering depends on the CSS you write for that selector. Examples: scale(1.6) to enlarge the circle over active elements; a different mix-blend-mode value per state; reduced opacity for a fade-out effect.
The .cursor-active class (CSS on #zone-circle) added by mouseenter, removed by mouseleave This class must carry cursor: none in your CSS — the JS never touches the cursor property directly. To hide the pointer outside the zone as well, move cursor: none to the body.

FAQ

The JS adds cursor-active to the #zone-circle container as soon as the pointer enters it. A CSS rule on that selector declares cursor: none — the JS never touches the property directly. On mouseleave, the class is removed and the native cursor immediately resumes. Without JavaScript, cursor-active is never added and the native pointer stays visible.
No. The code writes style.left = e.clientX + 'px' and style.top = e.clientY + 'px' directly on every mousemove, with no coefficient and no interpolation. The circle occupies the exact same position as the pointer at every instant — a geometric substitution, not a follower with friction. For a trail, a per-frame lerp or a CSS transition on left/top — that is a different effect.
Nothing, and that is consistent with the mechanics. On touch, mouseenter and mousemove do not fire: the circle stays at display: none, cursor-active is never added and the native cursor is never hidden. No disable code to write. To block emulated mousemove events some browsers fire on tap, gate the listeners behind matchMedia('(hover: hover) and (pointer: fine)').