hover-ui

Trail

A tapering chain of dots, each chasing the one ahead. The head swells over anything clickable.

npx shadcn@latest add https://hoverui.fun/r/trail.json

Adds components/cursors/trail.tsx. The CLI also installs the useCursor hook it runs on, once, into your hooks folder. The files are yours from there.

Draw through the empty space, then over the button: each dot chases the one ahead, and the head swells.

Kind
Followers
Dependencies
None
License
MIT

Usage

Any element
import { TrailCursor } from "@/components/cursors/trail";

export function Section() {
  return (
    <section>
      <TrailCursor />
      {/* this section now wears Trail */}
    </section>
  );
}

Put it inside <body> in your root layout to wear it on every page. Nested cursors take over inside their own element.

Props

className
Colors the cursor through currentColor, e.g. text-pink-500. To read on any background, pass text-white mix-blend-difference, as this site does.
length
How many dots make the chain. Default 14.
size
The head dot's diameter, in px; the rest taper from it. Default 12.
speed
How quickly each dot catches the one ahead; lower stretches the chain out. Default 30.
Hover targets
a, button, input, select, textarea, label, [role=button], [data-cursor-label]

Source

trail.tsx
"use client";

import { useRef } from "react";
import { cursorRoot, useCursor } from "@/hooks/use-cursor";

/** A tapering chain of dots, each chasing the one ahead; the head swells over anything clickable. */
export function TrailCursor({
  className = "",
  length = 14,
  size = 12,
  speed = 30,
}: {
  className?: string;
  /** How many dots make the chain. */
  length?: number;
  /** The head dot's diameter, in px; the rest taper from it. */
  size?: number;
  /** How quickly each dot catches the one ahead; lower stretches the chain out. */
  speed?: number;
}) {
  const dots = useRef<{ x: number; y: number }[]>([]);
  const swell = useRef(1);
  const ref = useCursor((p, el) => {
    // The chain follows `length` as it changes: new dots start at the pointer.
    while (dots.current.length < length) dots.current.push({ x: p.x, y: p.y });
    dots.current.length = length;
    swell.current += ((p.down ? 0.7 : p.hover ? 1.8 : 1) - swell.current) * p.ease(16);
    let { x, y } = p;
    dots.current.forEach((d, i) => {
      const k = i ? p.ease(speed) : 1;
      d.x += (x - d.x) * k;
      d.y += (y - d.y) * k;
      ({ x, y } = d);
      const t = 1 - i / length;
      // The swell fades down the chain: full at the head, none at the tail.
      (el.children[i] as HTMLElement).style.transform = `translate(${d.x}px, ${d.y}px) scale(${t * (1 + (swell.current - 1) * t)})`;
    });
  });

  return (
    <div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
      {Array.from({ length }, (_, i) => (
        <div key={i} className="absolute -translate-1/2 rounded-full bg-current" style={{ width: size, height: size }} />
      ))}
    </div>
  );
}

It runs on the useCursor hook, which has its own source and API.

More followers cursors

Made a cursor of your own? Contributed cursors carry their maker's name on their page.

Contribute a cursor