Liquid Cursor

PreviousNext

A smooth, fluid cursor that follows your mouse with gentle stretch and bounce effects.

Docs
paceuiui

Preview

Loading preview…
gsap/liquid-cursor.tsx
"use client";

import { ComponentProps, useRef } from "react";

import { useGSAP } from "@gsap/react";
import gsap from "gsap";

import { cn } from "@/lib/utils";

type LiquidCursorProps = Omit<ComponentProps<"div">, "children"> & {
    size?: number;
    strong?: boolean;
};

export const LiquidCursor = ({ size = 40, strong = false, className, ...props }: LiquidCursorProps) => {
    const cursorRef = useRef<HTMLDivElement>(null);
    const prevPos = useRef({ x: 0, y: 0 });
    const prevAngle = useRef(0);

    useGSAP(() => {
        const clickDrop = () => {
            if (!cursorRef.current) return;

            gsap.to(cursorRef.current, {
                scale: 1.3,
                duration: 0.3,
                ease: "power2.out",
                onComplete: () => {
                    gsap.to(cursorRef.current, {
                        scale: 1,
                        duration: 0.4,
                        ease: "bounce.out",
                    });
                },
            });
        };
        const moveDrop = (e: MouseEvent) => {
            if (!cursorRef.current) return;

            const dx = e.clientX - prevPos.current.x;
            const dy = e.clientY - prevPos.current.y;

            const distance = Math.sqrt(dx * dx + dy * dy);
            const angle = Math.atan2(dy, dx) * (180 / Math.PI);

            let delta = angle - prevAngle.current;
            if (delta > 180) delta -= 360;
            if (delta < -180) delta += 360;

            const smoothingFactor = 0.2;
            const smoothAngle = prevAngle.current + delta * smoothingFactor;

            const maxStretch = 1.2;
            const stretch = Math.min(distance / 30, maxStretch);

            const absDx = Math.abs(dx);
            const absDy = Math.abs(dy);
            const total = absDx + absDy || 1;
            const xRatio = absDx / total;

            const scaleX = 1 + xRatio * stretch;
            const scaleY = 1 - xRatio * stretch * 0.3;

            gsap.to(cursorRef.current, {
                duration: 1,
                left: e.clientX - size / 2,
                top: e.clientY - size / 2,
                scaleX,
                scaleY,
                rotate: smoothAngle,
                ease: "power2.out",
            });

            prevAngle.current = smoothAngle;
            prevPos.current = { x: e.clientX, y: e.clientY };
        };

        window.addEventListener("click", clickDrop);

        window.addEventListener("mousemove", moveDrop);
        return () => window.removeEventListener("mousemove", moveDrop);
    }, []);

    const lightStyle = {
        background: `
  radial-gradient(circle, 
    rgba(255, 255, 255, 0.25) 90%,  
    rgba(255, 255, 255, 0.1) 70%, 
    transparent 20%                
  )
`,
        border: "1px solid rgba(255, 255, 255, 0.25)",
    };

    const strongStyle = {
        background: `
    radial-gradient(125.95% 106.37% at 32.61% 3.41%,
    rgba(255, 255, 255, 0.6) 0%,
    rgba(255, 255, 255, 0.45) 28.13%,
    rgba(252, 252, 252, 0.35) 45.31%,
    rgba(248, 248, 248, 0.3) 66.67%,
    rgba(243, 243, 243, 0.25) 100%)
  `,
        boxShadow: `
    0 8px 16px rgba(0, 0, 0, 0.1),
    inset -4px -8px 12px rgba(255, 255, 255, 0.05),
    inset 3px 3px 8px rgba(240, 240, 240, 0.04),
    inset 5px 10px 14px rgba(255, 255, 255, 0.03)
  `,
        border: "1px solid rgba(255, 255, 255, 0.2)",
    };

    return (
        <div
            {...props}
            ref={cursorRef}
            className={cn(
                "pointer-events-none fixed z-999 rounded-full saturate-[180%] backdrop-blur-[2px]",
                "dark:saturate-[160%] dark:backdrop-brightness-[0.8]",
                className,
            )}
            style={{
                height: size,
                width: size,
                ...(strong ? strongStyle : lightStyle),
            }}
        />
    );
};

Installation

npx shadcn@latest add @paceui/liquid-cursor

Usage

import { LiquidCursor } from "@/components/ui/liquid-cursor"
<LiquidCursor />