webdevpuneet.com

Custom Cursor — Free HTML CSS JS Snippet

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

CategoryAnimations

Copy & paste

Source Code

<div class="stage" id="stage">
  <div class="cursor-dot" id="dot"></div>
  <div class="cursor-ring" id="ring"></div>

  <div class="content">
    <h1>Custom Cursor</h1>
    <p>Move your mouse to see the custom cursor with lag effect.</p>
    <div class="interactive-items">
      <button class="item-btn" data-cursor="click">Click me</button>
      <a href="#" class="item-link" data-cursor="link">Hover link</a>
      <div class="item-text" data-cursor="text">Select text</div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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.
Related: Gooey Text Morph
See the Gooey Text Morph for a related animations pattern worth pairing with this one.

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.