Cursor

An animated cursor component that allows you to customize both the cursor and cursor follow elements with smooth animations.

Made by Léo · Credits: imskyleen
Open in

Move your mouse over the div

demo-cursor.tsx
import {
  Cursor,
  CursorFollow,
  CursorProvider,
  type CursorFollowProps,
} from "@/components/unlumen-ui/cursor";

interface CursorDemoProps {
  global?: boolean;
  enableCursor?: boolean;
  enableCursorFollow?: boolean;
  side?: CursorFollowProps["side"];
  sideOffset?: number;
  align?: CursorFollowProps["align"];
  alignOffset?: number;
}

export const CursorDemo = ({
  global = false,
  enableCursor = true,
  enableCursorFollow = true,
  side = "bottom",
  sideOffset = 15,
  align = "end",
  alignOffset = 5,
}: CursorDemoProps) => {
  return (
    <div
      key={String(global)}
      className="max-w-[400px] h-[400px] w-full bg-accent rounded-lg flex items-center justify-center"
    >
      <p className="font-medium italic text-muted-foreground">
        Move your mouse over the div
      </p>
      <CursorProvider global={global}>
        {enableCursor && <Cursor />}
        {enableCursorFollow && (
          <CursorFollow
            side={side}
            sideOffset={sideOffset}
            align={align}
            alignOffset={alignOffset}
          >
            Designer
          </CursorFollow>
        )}
      </CursorProvider>
    </div>
  );
};

Installation

Install the following registry dependencies:

Copy and paste the following code into your project:

components/unlumen-ui/cursor.tsx
import * as React from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useVelocity,
  useTransform,
} from "motion/react";

import {
  CursorProvider as CursorProviderPrimitive,
  Cursor as CursorPrimitive,
  CursorFollow as CursorFollowPrimitive,
  CursorContainer as CursorContainerPrimitive,
  useCursor,
  type CursorProviderProps as CursorProviderPropsPrimitive,
  type CursorContainerProps as CursorContainerPropsPrimitive,
  type CursorProps as CursorPropsPrimitive,
  type CursorFollowProps as CursorFollowPropsPrimitive,
} from "@/components/unlumen-ui/primitives/animate/cursor";
import { cn } from "@/lib/utils";

type CursorProviderProps = Omit<CursorProviderPropsPrimitive, "children"> &
  CursorContainerPropsPrimitive;

function CursorProvider({ global, ...props }: CursorProviderProps) {
  return (
    <CursorProviderPrimitive global={global}>
      <CursorContainerPrimitive {...props} />
    </CursorProviderPrimitive>
  );
}

type CursorProps = Omit<CursorPropsPrimitive, "children" | "asChild">;

function Cursor({ className, ...props }: CursorProps) {
  const { cursorPos } = useCursor();
  const x = useMotionValue(0);
  const y = useMotionValue(0);

  React.useEffect(() => {
    x.set(cursorPos.x);
    y.set(cursorPos.y);
  }, [cursorPos, x, y]);

  const springConfig = { damping: 25, stiffness: 300 };
  const smoothX = useSpring(x, springConfig);
  const smoothY = useSpring(y, springConfig);

  const velocityX = useVelocity(smoothX);
  const velocityY = useVelocity(smoothY);

  // tilt the cursor based on velocity for a gravity feel
  const rotateVelocity = useTransform([velocityX, velocityY], ([vx, vy]) => {
    const rx = ((vx as number) / 1000) * 30;
    const ry = ((vy as number) / 1000) * 30;
    return Math.max(-45, Math.min(45, rx + ry));
  });
  const rotate = useSpring(rotateVelocity, { damping: 15, stiffness: 200 });

  const scale = useTransform([velocityX, velocityY], ([vx, vy]) => {
    const velocity = Math.sqrt((vx as number) ** 2 + (vy as number) ** 2);
    return 1 - Math.min(velocity / 2000, 0.1);
  });

  return (
    <CursorPrimitive
      style={{
        top: smoothY,
        left: smoothX,
        transform: "translate(-4.5%, -11%)", // Override default centering to align tip
      }}
      {...props}
    >
      <motion.svg
        className={cn("size-6 text-foreground", className)}
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 40 40"
        style={{
          rotate,
          scale,
          transformOrigin: "4.5% 11%", // Tip of the SVG path
        }}
      >
        <path
          fill="currentColor"
          d="M1.8 4.4 7 36.2c.3 1.8 2.6 2.3 3.6.8l3.9-5.7c1.7-2.5 4.5-4.1 7.5-4.3l6.9-.5c1.8-.1 2.5-2.4 1.1-3.5L5 2.5c-1.4-1.1-3.5 0-3.3 1.9Z"
        />
      </motion.svg>
    </CursorPrimitive>
  );
}

type CursorFollowProps = Omit<CursorFollowPropsPrimitive, "asChild">;

function CursorFollow({
  className,
  children,
  sideOffset = 15,
  alignOffset = 5,
  ...props
}: CursorFollowProps) {
  const { cursorPos } = useCursor();
  const x = useMotionValue(0);
  const y = useMotionValue(0);

  React.useEffect(() => {
    x.set(cursorPos.x);
    y.set(cursorPos.y);
  }, [cursorPos, x, y]);

  const springConfig = { damping: 25, stiffness: 300 };
  const smoothX = useSpring(x, springConfig);
  const smoothY = useSpring(y, springConfig);

  const velocityX = useVelocity(smoothX);
  const velocityY = useVelocity(smoothY);

  const scaleX = useTransform(velocityX, [-1000, 0, 1000], [0.9, 1, 1.15]);
  const scaleY = useTransform(velocityY, [-1000, 0, 1000], [1.15, 1, 0.9]);

  const skewX = useTransform(velocityX, [-1000, 0, 1000], [-3, 0, 3]);
  const skewY = useTransform(velocityY, [-1000, 0, 1000], [-3, 0, 3]);

  return (
    <CursorFollowPrimitive
      sideOffset={sideOffset}
      alignOffset={alignOffset}
      {...props}
    >
      <motion.div
        className={cn(
          "bg-foreground rounded-md text-background px-2 py-1 text-sm",
          className,
        )}
        style={{
          scaleX,
          scaleY,
          skewX,
          skewY,
        }}
      >
        {children}
      </motion.div>
    </CursorFollowPrimitive>
  );
}

export {
  CursorProvider,
  Cursor,
  CursorFollow,
  type CursorProviderProps,
  type CursorProps,
  type CursorFollowProps,
};

Update the import paths to match your project setup.

Usage

<CursorProvider>
  <Cursor />
  <CursorFollow>Designer</CursorFollow>
</CursorProvider>

API Reference

CursorProvider

children
React.ReactNode

The children of the CursorProvider

global?
boolean
false

Whether the cursor is global

...props?
HTMLMotionProps<"div">

The props of the CursorContainer

Cursor

asChild?
boolean
false

Change the default rendered element for the one passed as a child, merging their props and behavior.

...props?
HTMLMotionProps<"div">

The props of the CursorContainer

CursorFollow

asChild?
boolean
false

Change the default rendered element for the one passed as a child, merging their props and behavior.

side?
CursorFollowSide
bottom

The side of the CursorFollow

sideOffset?
number
15

The side offset of the CursorFollow

align?
CursorFollowAlign
end

The align of the CursorFollow

alignOffset?
number
5

The align offset of the CursorFollow

transition?
SpringOptions
{ stiffness: 500, damping: 50, bounce: 0 }

The transition of the CursorFollow

...props?
HTMLMotionProps<"div">

The props of the CursorFollow

Built by Léo from Unlumen :3

Last updated: 4/29/2026