Custom Cursor — Free HTML CSS JS Snippet

Custom Cursor · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

8px dot at exact cursor position each frame — instant response
36px ring uses lerp rx += (mx-rx)*0.12 — smooth follow with natural deceleration
cursor: none on stage hides OS cursor; pointer-events: none on both elements
.hovering class on ring expands to 60px and reduces opacity on interactive elements
requestAnimationFrame loop updates both dot and ring positions per frame
mouseenter/mouseleave on buttons and links toggle .hovering state
position: fixed on both elements — unaffected by page scroll
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Custom Cursor — Dot + Lagging Ring via Linear Interpolation

Screenshot of the Custom Cursor snippet rendered live

Custom cursor replaces the default OS cursor with a branded dot and lagging ring — the ring follows with a smooth delay creating an elastic feel. It pairs with a cursor-following spotlight effect. Used on creative portfolios, agency sites, and premium dark interfaces.

The lerp (linear interpolation) lag

The dot (8px) is set to the exact mouse position each frame: dot.style.left = mx + "px". The ring (36px) uses lerp: rx += (mx - rx) * 0.12. Each frame the ring moves 12% of the remaining distance to the cursor — it accelerates when far and decelerates as it catches up, creating organic following motion.

cursor: none and pointer-events: none

cursor: none on the stage hides the OS cursor. Both elements have pointer-events: none so mouse events pass through to page elements.

Context-aware hover state

Buttons and links have mouseenter/mouseleave listeners that toggle .hovering on the ring — expanding it to 60px and reducing opacity, communicating a clickable element is under the cursor.

The requestAnimationFrame loop

animateLoop() is called via requestAnimationFrame, creating a persistent animation loop that runs every frame. Inside the loop, mx and my (current mouse position) are read from module-level variables updated by mousemove. The dot moves to the exact position; the ring lerps toward it. Both use transform: translate() for GPU-composited movement with no layout recalculation.

Click feedback state

A mousedown listener adds .clicking to the ring — scaling it down to 0.7 and changing its background to a semi-transparent fill. mouseup removes the class. This gives a physical "press" sensation to the cursor.

When to use a custom cursor

Custom cursors work best on creative, dark-themed pages where the OS cursor is less visible and where the branded element reinforces the product identity. They should always be disabled on touch devices via @media (pointer: coarse) { .cursor-dot, .cursor-ring { display: none } } since touch screens have no cursor.

Accessibility note

Custom cursors can reduce accessibility by hiding the familiar OS pointer. Always maintain cursor: none only on the specific stage element, not on the entire page, so browser UI elements (scrollbars, select dropdowns) keep their native cursor.

Reducing motion preference

For users who have enabled "Reduce Motion" in their OS settings, the lerp animation and cursor ring should be simplified. Add: if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { ring.style.display = "none"; } This hides the lagging ring, keeping only the dot which follows the cursor directly without animation.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to reason about the lerp math from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why rx += (mx - rx) * 0.12 produces deceleration as the ring approaches the cursor, and why the dot and ring are updated inside a requestAnimationFrame loop rather than directly inside the mousemove listener. The same assistant is useful for optimizing it — ask whether reading mx and my from module-level variables versus a ref matters for performance in a React port, and whether the animation loop should pause entirely when the mouse hasn't moved in a while to save battery on laptops. It's also a good way to extend the cursor: ask it to add a magnetic pull effect where the ring snaps toward the center of whatever button it's hovering, a trailing particle effect behind the dot, or a text-selection beam cursor state for editable content. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a custom cursor with a dot-and-ring pair in plain HTML, CSS, and JavaScript using linear interpolation and requestAnimationFrame — no animation library.

Requirements:
- Two fixed-position, pointer-events: none elements layered above the page: a small dot and a larger ring, both hidden from normal document flow and unaffected by page scroll.
- Hide the real OS cursor with cursor: none on the interactive stage only, not on the entire page, so native browser UI like scrollbars and select dropdowns keep their normal cursor.
- Track the live mouse position in module-level (or ref-based) variables updated by a mousemove listener, without directly setting any element's position inside that listener.
- Run a persistent requestAnimationFrame loop that sets the dot's position to the exact current mouse coordinates every frame, and moves the ring's position toward the mouse coordinates using linear interpolation: increment the ring's position by a fraction (roughly 0.1 to 0.15) of the remaining distance each frame, so the ring visibly lags and eases in behind the dot rather than snapping to it.
- Add a mousedown/mouseup pair that toggles a class shrinking or recoloring the cursor for tactile press feedback, and mouseenter/mouseleave listeners on buttons and links that toggle a class expanding the ring to signal an interactive element.
- Add a media query for pointer: coarse that fully hides the custom cursor elements and restores the native cursor, since touch devices have no persistent pointer to track.
- Initialize the tracked mouse position to the center of the viewport (not 0,0) before the first mousemove event fires, so the cursor doesn't visibly jump in from the top-left corner on page load.

Step by step

How to Use

  1. 1
    Move the cursor in the previewMove the cursor around the dark stage to see the dot track instantly and the ring follow with a lag. Hover over buttons to see the ring expand.
  2. 2
    Change the lag speedUpdate the 0.12 lerp factor in the JS panel. Higher (0.2) responds faster; lower (0.05) lags more.
  3. 3
    Change cursor coloursUpdate background on .cursor-dot and border-color on .cursor-ring in the CSS panel.
  4. 4
    Add hover state to more elementsUpdate the querySelectorAll selector in the JS to include any elements that should expand the ring.
  5. 5
    Change ring hover sizeUpdate width/height on .cursor-ring.hovering in the CSS panel.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Creative agency and portfolio sites
A custom cursor with a lagging ring signals craftsmanship — that every detail of the interface has been considered. The ring expansion on hover over buttons communicates interactivity without additional UI labels or tooltips.
Dark premium product interfaces
On dark-themed SaaS products, dev tools, and analytics dashboards, replacing the OS cursor with a custom one creates a cohesive branded experience. The subtle indigo dot and ring match dark interface colour palettes.
Learn lerp (linear interpolation) in animation
rx += (mx - rx) * 0.12 is one of the most useful animation patterns in frontend development. Edit the 0.12 factor from 0.05 to 0.5 and observe how the lag changes from heavy elastic to near-instant. This pattern appears in scroll animations, camera following, and spring physics.
Interactive showcase and feature demo pages
On feature demo pages where users explore product capabilities, a custom cursor draws attention to interactive areas. The ring expanding on hoverable elements guides user attention without tooltip clutter.
Game and entertainment web interfaces
Browser games and entertainment sites use custom cursors to match their theme and genre. A crosshair cursor for a shooter, a wand for a magic-themed app, or a custom pointer for a design tool — the pattern is the same regardless of the visual style.
Multi-state cursor with context awareness
Extend with additional states: a text beam on contenteditable elements, a grab hand on draggable cards, a zoom-in on zoomable images, and a loading spinner on async operations. Each state needs its own CSS class on the ring element.

Got questions?

Frequently Asked Questions

Linear interpolation: rx += (mx - rx) * 0.12. Each requestAnimationFrame, rx moves 12% of the remaining distance to the cursor position. When the cursor is far away, 12% of a large gap is a big step. When close, 12% of a small gap is tiny — creating natural ease-out deceleration without an animation library.

mousemove fires at irregular intervals tied to mouse movement speed. requestAnimationFrame fires at the display refresh rate (60fps) regardless of mouse activity. Running lerp in rAF produces consistent smooth motion. If you used mousemove, the ring would stutter during fast mouse movements.

cursor: none only applies to pointer devices. Add @media (pointer: coarse) { .cursor-dot, .cursor-ring { display: none !important; } * { cursor: auto !important; } } to restore the native cursor on touch screens. pointerType in pointer events can also detect touch vs mouse at runtime.

On page load before the first mousemove, the dot and ring are at position 0,0. Fix this by initialising positions before the first frame: let mx = window.innerWidth/2, my = window.innerHeight/2, rx = mx, ry = my. This places both at the screen centre until the first real mouse event.

Add event listeners on specific elements: document.querySelectorAll("img[data-zoomable]").forEach(el => { el.addEventListener("mouseenter", () => ring.classList.add("zoom")); el.addEventListener("mouseleave", () => ring.classList.remove("zoom")); }). CSS .cursor-ring.zoom { transform: scale(1.8); } handles the visual change.

Yes. Use useRef for the dot and ring elements. Use useEffect to add the mousemove listener and start the rAF loop. Store mx/my in useRef (not useState) to avoid re-renders on every mouse move — a critical performance detail for cursor animations that update every frame.