Skip to content

Web Development

Custom Cursors with mouse-follower — The $200k Site Detail That's Actually 12kb of JS

Every premium agency site has a custom cursor. It costs nothing (12kb library) and costs everything (users notice when it's missing). The pattern: subtle hover states, disable on touch, respect `prefers-reduced-motion`, show real cursor on inputs.

🎯 💎

Open Locomotive's site. Move your mouse. The cursor isn't a browser arrow anymore — it's a glowing orb that grows when you hover links, shrinks on hover-away. Open Cuberto. The cursor trails with a physics-based follow. Open Studio Lumio. It's a dot that changes color on interactive elements. These aren't gimmicks. They're $200k-site touches. They're the details that make your brain say "this is premium" without knowing why.

Here's the plot twist: they're all using the same 12kb JavaScript library called mouse-follower. It's open source. You can ship it today. And unlike Lenis (which hijacks your scroll), mouse-follower is pure surface-level UX. It doesn't break anything. It only adds. It disables on touch. It respects accessibility flags. It's the most risk-free premium-feel upgrade you can make.

What Is a Custom Cursor, Actually?

By default, your browser owns the cursor. OS paints it. Browser hides it on `input` fields. You can't hook into it. Custom cursors work by hiding the real cursor (`cursor: none` or `pointer-events: none`) and replacing it with a fake one — a div that tracks mouse position in JavaScript. When the user moves their mouse, you update the div position using `requestAnimationFrame`. It follows the cursor. It feels instant. It feels premium.

The fake cursor can change size, color, opacity, rotation — anything. Hover a link? Grow the dot. Hover an image? Show a zoom icon inside. Hover a draggable? Show a "drag" hint. Hover an input? Show the real cursor so users can still see their text selection. This is why agencies love custom cursors: they're a second layer of interaction feedback. They whisper "this element responds" without being loud.

Why Every Premium Site Uses This

Conversion psychology. When you move your mouse, your brain gets tactile feedback. On premium sites, that feedback is polished. Your cursor doesn't snap to elements — it floats and grows. It feels expensive. It feels intentional. A/B tests show custom cursors increase engagement by 8–15% (no, really — Webflow's case study proved it). The reason: users spend 2–3 seconds longer on the page because interaction feels rewarding.

You're not forcing this. You're not slowing the site down. You're just replacing a system-level UI element with a custom one that feels better. It's the same reason luxury car dashboards have animated screens and haptic feedback. The car still drives. But the experience feels worth the price.

mouse-follower Setup (7 Lines)

import MouseFollower from 'mouse-follower';

const cursor = new MouseFollower({
  el: '.cursor',
  container: document.body,
  speed: 0.6,
  ease: 'expo.out',
});

cursor.init();

That's your whole setup. Create a div with class `cursor`. Pass it to MouseFollower. Set speed (0.6 = 60% catch-up speed, higher = snappier). Set ease (GSAP easing string — `expo.out`, `power2.inOut`, etc.). Call `init()`. Your cursor is now custom.

The HTML is a single div: `<div class="cursor"></div>`. Styling is pure CSS — size, background, border radius. No magic. The JavaScript just moves it.

Four Hover-State Patterns That Convert

Pattern 1: Link Grow (Most Common)

const cursor = new MouseFollower({
  el: '.cursor',
  speed: 0.6,
});

// Grow on link hover
document.querySelectorAll('a').forEach(link => {
  link.addEventListener('mouseenter', () => {
    cursor.setData({
      scale: 1.4, // grow 40%
      opacity: 0.8,
    });
  });
  link.addEventListener('mouseleave', () => {
    cursor.setData({
      scale: 1,
      opacity: 0.6,
    });
  });
});

Simplest pattern. Every `<a>` tag causes the cursor to grow when you hover it. Off-hover, it shrinks. Users learn: "this glowing dot grows on clickables." Instant affordance. No tooltip needed.

Pattern 2: Button Text Inside Cursor

const cursor = new MouseFollower({
  el: '.cursor',
  speed: 0.8,
});

const cta = document.querySelector('a.cta-button');
cta.addEventListener('mouseenter', () => {
  cursor.setData({
    scale: 2.5,
    text: 'Click',
    backgroundColor: '#fff',
    color: '#000',
  });
});
cta.addEventListener('mouseleave', () => {
  cursor.setData({
    scale: 1,
    text: '',
    backgroundColor: 'rgba(255, 255, 255, 0.2)',
  });
});

Premium move. On CTA hover, the cursor balloons and shows a "Click" label inside. Off-hover, it returns to normal. This is what Cuberto does. It's obnoxious on every link, but surgical on CTAs. Use sparingly.

Pattern 3: Image Zoom Icon

document.querySelectorAll('img').forEach(img => {
  img.addEventListener('mouseenter', () => {
    cursor.setData({
      scale: 1.8,
      text: '🔍', // or SVG icon
      backgroundColor: 'transparent',
    });
  });
  img.addEventListener('mouseleave', () => {
    cursor.setData({
      scale: 1,
      text: '',
    });
  });
});

Gallery sites eat this up. Hover an image, the cursor shows a magnifying glass emoji (or a custom SVG zoom icon). Users instantly know: "click to expand." No hover text overlay needed.

Pattern 4: Drag Hint (for Sortable/Draggable Elements)

document.querySelectorAll('[draggable="true"]').forEach(el => {
  el.addEventListener('mouseenter', () => {
    cursor.setData({
      scale: 1.3,
      backgroundColor: '#fbbf24', // amber for "draggable"
      opacity: 1,
    });
  });
  el.addEventListener('mouseleave', () => {
    cursor.setData({
      scale: 1,
      backgroundColor: 'rgba(255, 255, 255, 0.2)',
    });
  });
});

Less common but powerful on internal tools. Draggable elements get a different cursor color. Users know they can grab and move. No magic — just semantic color feedback.

Mobile & Accessibility Gates

Custom cursors only work on desktop. On touch devices, there is no cursor. If you initialize mouse-follower on mobile, you've added 12kb of dead weight. Worse, if you don't restore the real cursor on inputs, users can't see their text selection. The experience breaks.

const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);

if (!isMobile) {
  const cursor = new MouseFollower({
    el: '.cursor',
    speed: 0.6,
  });
  cursor.init();
} else {
  // On mobile, show cursor on all elements (default)
  document.documentElement.style.cursor = 'auto';
}

Or use a media query:

const isSmallScreen = window.matchMedia('(max-width: 768px)').matches;

if (!isSmallScreen) {
  cursor.init();
}

For accessibility, always restore the real cursor on form inputs. Users with motor disabilities often use eye-trackers or switch controls. They need to see the text cursor in input fields.

document.querySelectorAll('input, textarea, [contenteditable]').forEach(input => {
  input.addEventListener('mouseenter', () => {
    cursor.setData({
      scale: 1,
      visible: false, // hide custom cursor
    });
    input.style.cursor = 'text'; // show real cursor
  });
  input.addEventListener('mouseleave', () => {
    cursor.setData({
      visible: true,
    });
    input.style.cursor = 'none';
  });
});

Respect `prefers-reduced-motion` too. If a user opts for reduced motion, disable the custom cursor entirely:

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion && !isMobile) {
  cursor.init();
}

Six FAQs

Does mouse-follower work with Astro?

Yes. Load it in a client component or a script tag with `is:inline`. Initialize it after DOM is ready. Works with ViewTransitions too — just reinitialize the hover listeners on page change.

What if I want a trail instead of a dot?

mouse-follower supports this out of the box. Pass `type: 'trail'` and it will create a tail effect following your cursor. Slower devices will stutter — use sparingly.

Does it work with `pointer-events: none`?

Yes. That's the pattern — hide the real cursor with `cursor: none`, show the fake one with `pointer-events: auto`. The fake cursor doesn't block clicks.

Can I animate the cursor with GSAP?

Absolutely. Hook into `MouseFollower` events and trigger GSAP timelines. When cursor enters a link, animate its scale, rotation, or color. Pair it with ScrollTrigger for scroll-driven cursor effects (unusual but possible).

What about performance?

12kb gzipped, frame-locked on `mousemove`. On modern hardware, negligible. On older devices, the cursor might lag by 1–2 frames. Test on a slow device. If it stutters, reduce scale animations or disable on that device.

Is a custom cursor worth the complexity?

On premium sites? Absolutely. On MVPs? No. On a landing page selling $5k products? Yes. On a utility app? No. Use it where the budget and brand justify it. Velocity X includes custom cursors for exactly this reason — we charge by the feel, not the code.

The Bottom Line

A custom cursor costs 12kb and zero accessibility debt if done right. It converts. Every premium site has one because users expect it. It's the difference between "this site is professional" and "this site is premium." Hide it on mobile, restore it on inputs, respect motion preferences, and your cursor becomes invisible magic — the kind of polish that makes your site feel like it costs 10x what you charged.

Want to see custom cursors in a full Astro site? Check out how smooth scroll + custom cursors work together to create that Locomotive-agency-site feel. Pair them and your site doesn't just look premium — users feel it.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.