EmpireUI
Get Pro
← Blog7 min read#cursor-animation#css-animation#react-effects

Custom Cursor Trail Animation: Interactive Pointer Effects

Build custom cursor trail animations with React and Tailwind v4. Interactive pointer effects that respond to mouse movement, with smooth particle trails and zero deps.

Abstract glowing cursor trail animation on dark background with colorful light streaks

Why Cursor Trails Still Work in 2026

Honestly, cursor trail animations are one of those effects that developers dismiss as gimmicky — right until they see a well-executed one on a portfolio or landing page and immediately want to know how it's built. They're not just decoration. A trail gives the user immediate visual feedback that the interface is alive and responding to them.

The trick is restraint. A bloated trail that renders 60 DOM nodes and tanks your FPS isn't worth shipping. But a lightweight canvas-based or CSS-driven trail that adds 2ms to your input latency? That's a different story entirely.

We're going to build something real here — a React hook-based cursor trail that works with Tailwind v4.0.2, handles cleanup properly, and doesn't fight your existing layout. No libraries needed. Just useRef, useEffect, and a tiny bit of math.

How Cursor Trail Animations Actually Work

At the core, a cursor trail is just a history of mouse positions rendered with a slight delay. You track mousemove events, push coordinates into a ring buffer, then render each point as a fading element — either DOM nodes, SVG circles, or canvas pixels.

The two most common approaches are DOM-based (absolutely positioned div elements with CSS transitions) and canvas-based (a <canvas> element you repaint on requestAnimationFrame). DOM-based is easier to style with Tailwind. Canvas-based scales better when you want 50+ trail particles without melting the main thread.

For most use cases — agency sites, SaaS dashboards, portfolio pages — DOM-based with 8–12 trail points hits the sweet spot. That's what we're building here. If you're chasing something more particle-heavy, check out particles background for React which handles the canvas side efficiently.

The Core useCursorTrail Hook

Here's the hook. It tracks mouse position and maintains a fixed-length array of trail points. Each point gets an age, which drives the opacity and scale down to zero as it fades out.

import { useEffect, useRef, useState } from 'react';

type TrailPoint = { x: number; y: number; id: number };

const TRAIL_LENGTH = 12;
const POINT_LIFETIME_MS = 400;

export function useCursorTrail() {
  const [trail, setTrail] = useState<TrailPoint[]>([]);
  const counterRef = useRef(0);

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      const point: TrailPoint = {
        x: e.clientX,
        y: e.clientY,
        id: counterRef.current++,
      };

      setTrail((prev) => {
        const next = [...prev, point];
        return next.slice(-TRAIL_LENGTH);
      });

      // Auto-expire old points
      setTimeout(() => {
        setTrail((prev) => prev.filter((p) => p.id !== point.id));
      }, POINT_LIFETIME_MS);
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return trail;
}

The setTimeout cleanup is intentional — it lets each point self-expire rather than requiring a global animation loop. This keeps the hook stateless enough to drop into any component without side effects leaking across re-renders.

Rendering the Trail with Tailwind Classes

The hook gives you coordinates. Now you need to paint them. Each trail point becomes an absolutely positioned element. The further back in the array, the smaller and more transparent it should be. We calculate that from the index.

import { useCursorTrail } from './useCursorTrail';

export function CursorTrail() {
  const trail = useCursorTrail();

  return (
    <>
      {trail.map((point, index) => {
        const progress = index / trail.length; // 0 = oldest, 1 = newest
        const size = 6 + progress * 14; // 6px → 20px
        const opacity = 0.08 + progress * 0.72; // fades in toward cursor

        return (
          <div
            key={point.id}
            className="pointer-events-none fixed z-[9999] rounded-full"
            style={{
              left: point.x,
              top: point.y,
              width: size,
              height: size,
              opacity,
              transform: 'translate(-50%, -50%)',
              background: `rgba(139, 92, 246, ${opacity})`, // violet-500 base
              boxShadow: `0 0 ${size * 1.5}px rgba(139, 92, 246, ${opacity * 0.6})`,
              transition: 'opacity 80ms ease-out',
            }}
          />
        );
      })}
    </>
  );
}

Drop <CursorTrail /> right inside your root layout, and it renders on top of everything via z-[9999]. The pointer-events-none is non-negotiable — without it you'll interfere with every click on the page.

Want a different color scheme? Swap the rgba(139, 92, 246, ...) values. For a cyan glow that pairs well with dark glassmorphism UIs (see what is glassmorphism for that aesthetic), use rgba(6, 182, 212, ...) which maps to Tailwind's cyan-500.

CSS-Only Cursor Trail with Custom Properties

Don't want to touch JavaScript at all? You can get a basic cursor trail feel with pure CSS using @property and animation-delay chains. It's more limited but ships zero JS.

@property --cursor-x {
  syntax: '<length>';
  inherits: true;
  initial-value: 0px;
}

@property --cursor-y {
  syntax: '<length>';
  inherits: true;
  initial-value: 0px;
}

.cursor-dot {
  position: fixed;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.15);
  pointer-events: none;
  left: var(--cursor-x);
  top: var(--cursor-y);
  translate: -50% -50%;
  transition:
    left 60ms linear,
    top 60ms linear,
    opacity 200ms ease;
}

.cursor-dot:nth-child(2) { transition-delay: 30ms; opacity: 0.7; }
.cursor-dot:nth-child(3) { transition-delay: 60ms; opacity: 0.5; }
.cursor-dot:nth-child(4) { transition-delay: 90ms; opacity: 0.3; }
.cursor-dot:nth-child(5) { transition-delay: 120ms; opacity: 0.15; }

You'd still need a tiny JS snippet to update --cursor-x and --cursor-y on the :root element via document.documentElement.style.setProperty. But the actual animation — including the staggered delay trail effect — is purely CSS. This approach plays nicely with Tailwind v4.0.2's native CSS custom property support.

Performance: Keeping Cursor Animations Smooth

The single biggest mistake with cursor effects is forcing layout during the mouse event handler. Don't read offsetWidth, don't call getBoundingClientRect(), don't do anything that triggers a reflow. Just write values — either to state or to CSS custom properties.

For the DOM-based approach, every trail point update triggers a React re-render. With 12 points expiring over 400ms, you're looking at roughly 30 renders per second during active movement. That sounds scary. It's actually fine, because each render only updates a tiny subtree in a portal. But if you're targeting lower-end devices or running next to a heavy animation like aurora background, consider throttling your mousemove handler to every 16ms with a timestamp check.

Canvas-based rendering sidesteps the re-render issue entirely. You paint directly in a requestAnimationFrame loop, read from a ref (not state), and React never even knows the animation is happening. The trade-off is that Tailwind classes can't style canvas contents — you're back to raw canvas API calls. Worth it for 50+ particles. Overkill for 12.

Also: add a prefers-reduced-motion media query check. Users who've opted into reduced motion don't want trails following them around. Respect that.

Customizing Trail Shape, Color, and Behavior

The violet glow is just a starting point. Here are a few variations that ship well in production. A white-to-transparent gradient trail (rgba(255,255,255,0.85) at the head down to rgba(255,255,255,0.04) at the tail) works on colored backgrounds without clashing. A ring-style trail — where each point is border only with no fill — gives a more minimal, editorial feel. A trail that changes color based on velocity is genuinely fun: calculate distance between the last two points, map it to a hue rotation, done.

You can also vary the shape. Replace the border-radius: 50% circles with short rotated rectangles whose rotation follows the movement direction. Calculate the angle with Math.atan2(dy, dx) and apply it as a CSS rotate transform. The effect looks like light streaks rather than dots, which pairs well with the shooting stars background aesthetic.

For theme-aware trails, hook into your theme context (or read from a data-theme attribute) and swap colors accordingly. If you're already managing dark/light mode, check out theme toggle in React — the same pattern works here. Store trail color as a CSS custom property on :root and let the trail component just reference var(--trail-color).

Integrating the Cursor Trail Into Your Next.js App

The cleanest integration point is app/layout.tsx. Render <CursorTrail /> once, outside any scrollable container, and it'll apply globally across all routes. Because it uses position: fixed, scroll doesn't affect it.

One thing to watch: server-side rendering. The hook calls window.addEventListener, which doesn't exist on the server. Wrap the component in a dynamic import with ssr: false in Next.js, or guard the effect with a typeof window !== 'undefined' check. Either works.

If you're hiding the native cursor with cursor: none to replace it entirely with your custom element, make sure you restore it on touch devices — mobile users don't have a cursor to hide, and setting cursor: none breaks some browser UI on iOS. A simple @media (hover: none) rule that resets to cursor: auto handles this.

FAQ

Does the cursor trail animation affect scroll performance?

Not if you're using position: fixed elements and avoiding layout reads in the mouse handler. The trail elements sit outside the document flow entirely, so scrolling doesn't trigger repaints on them. Where you can run into trouble is if you're also animating the scroll container — composite those separately.

How do I hide the default browser cursor and replace it with a custom one?

Add cursor: none to the element where you want the custom cursor active — usually body or a specific section. Then render your custom cursor element as a fixed-position div that tracks mousemove. Don't forget @media (hover: none) { cursor: auto; } to restore the default on touch devices.

Can I use cursor trail effects with React Server Components?

No — cursor effects require window and event listeners, which only exist in the browser. Mark the component with 'use client' at the top of the file, or use next/dynamic with ssr: false. The hook itself also needs to live in a client component.

What's the best trail length for smooth performance without lag?

8–14 points works well for most DOM-based implementations. Below 8 the trail looks choppy at fast movement speeds. Above 16 you start seeing noticeable re-render overhead, especially on lower-end hardware. If you need more points, switch to canvas rendering and use a ref instead of state for the trail buffer.

How do I make the cursor trail respect prefers-reduced-motion?

Check window.matchMedia('(prefers-reduced-motion: reduce)').matches before attaching the mousemove listener, or conditionally render the component. You can also just set opacity: 0 on the trail container via a CSS media query — @media (prefers-reduced-motion: reduce) { .cursor-trail { display: none; } } — which is the least invasive approach.

Can I change the trail color dynamically, like on hover over different sections?

Yes. Store the trail color in a React context or as a CSS custom property on :root. When the user hovers a section, update that value. The trail elements read from it via var(--trail-color) in their inline styles or CSS. This avoids prop-drilling the color through your entire component tree.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Magnetic Button Hover Effect: CSS + JS Cursor-Following AnimationCSS Flip Card Animation: 3D Reveal Without JavaScriptTailwind Animation Utilities: Built-In Classes and Custom KeyframesAurora UI Effects: Animated Northern Lights in CSS and React