Components
Cursors
Premium and fun custom cursor effects.
Preview
Move your mouse to test the active effect
Current: fluid
Variants
1. Fluid Cursor
A high-performance trailing effect using Canvas. Best for landing pages.
components/cursors/FluidCursor.tsx
"use client";import { useEffect, useRef } from "react";import { usePathname } from "next/navigation";export default function FluidCursor({ forceVisible = false }: { forceVisible?: boolean }) {const canvasRef = useRef<HTMLCanvasElement>(null);const pathname = usePathname();useEffect(() => {// If not forced and not on landing page, don't runif (!forceVisible && pathname !== "/") return;// Don't run on mobileif (typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches) return;const canvas = canvasRef.current;if (!canvas) return;const ctx = canvas.getContext("2d");if (!ctx) return;let width = window.innerWidth;let height = window.innerHeight;const resize = () => {width = window.innerWidth;height = window.innerHeight;canvas.width = width;canvas.height = height;};window.addEventListener("resize", resize);resize();const mouse = { x: width / 2, y: height / 2 };const trail: { x: number; y: number; age: number }[] = [];const maxTrail = 30; // Length of trailconst stiffness = 0.2; // Spring stiffness// Smooth positionconst current = { x: width / 2, y: height / 2 };const onMouseMove = (e: MouseEvent) => {mouse.x = e.clientX;mouse.y = e.clientY;};window.addEventListener("mousemove", onMouseMove);const render = () => {// Spring smoothingcurrent.x += (mouse.x - current.x) * stiffness;current.y += (mouse.y - current.y) * stiffness;// Add to trailtrail.push({ x: current.x, y: current.y, age: 0 });if (trail.length > maxTrail) trail.shift();ctx.clearRect(0, 0, width, height);// Draw trailif (trail.length > 2) {ctx.lineCap = "round";ctx.lineJoin = "round";// Fluid effect with blurfor (let i = 0; i < trail.length - 1; i++) {const p1 = trail[i];const p2 = trail[i + 1];const ratio = i / trail.length;p1.age++;ctx.beginPath();ctx.moveTo(p1.x, p1.y);ctx.lineTo(p2.x, p2.y);// Outer glowctx.shadowBlur = 20 * ratio;ctx.shadowColor = "rgba(255, 255, 255, 0.5)";// Variable widthconst lineWidth = 12 * ratio;ctx.lineWidth = lineWidth;// White core with fading tailctx.strokeStyle = `rgba(255, 255, 255, ${ratio * 0.4})`;ctx.stroke();}}requestAnimationFrame(render);};const animId = requestAnimationFrame(render);return () => {window.removeEventListener("resize", resize);window.removeEventListener("mousemove", onMouseMove);cancelAnimationFrame(animId);};}, [pathname, forceVisible]);if (!forceVisible && pathname !== "/") return null;return (<canvasref={canvasRef}className="fixed inset-0 pointer-events-none z-[9999] mix-blend-difference hidden md:block"/>);}
2. Glow Cursor
A smooth, spring-animated glowing orb. Minimalist and clean.
components/cursors/GlowCursor.tsx
"use client";import { useEffect, useState } from "react";import { motion, useMotionValue, useSpring } from "framer-motion";import { usePathname } from "next/navigation";export default function GlowCursor({ forceVisible = false }: { forceVisible?: boolean }) {const [isVisible, setIsVisible] = useState(false);const cursorX = useMotionValue(-100);const cursorY = useMotionValue(-100);const pathname = usePathname();const springConfig = { damping: 25, stiffness: 400, mass: 0.5 };const cursorXSpring = useSpring(cursorX, springConfig);const cursorYSpring = useSpring(cursorY, springConfig);useEffect(() => {if (!forceVisible && pathname !== "/") return;if (typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches) return;const moveCursor = (e: MouseEvent) => {cursorX.set(e.clientX - 16); // Center the 32px cursorcursorY.set(e.clientY - 16);if (!isVisible) setIsVisible(true);};window.addEventListener("mousemove", moveCursor);return () => {window.removeEventListener("mousemove", moveCursor);};}, [cursorX, cursorY, isVisible, pathname, forceVisible]);if (!forceVisible && pathname !== "/") return null;if (!isVisible && !forceVisible && pathname !== "/") return null;return (<motion.divclassName="fixed top-0 left-0 w-8 h-8 bg-white/50 rounded-full pointer-events-none z-[9999] hidden md:block blur-[2px]"style={{translateX: cursorXSpring,translateY: cursorYSpring,boxShadow: `0 0 15px 2px rgba(255, 255, 255, 0.6),0 0 30px 10px rgba(255, 255, 255, 0.3),0 0 60px 20px rgba(255, 255, 255, 0.1)`,mixBlendMode: "hard-light"}}/>);}
3. Spotlight Cursor
A large, premium light source using mix-blend-mode for a realistic flashlight effect.
components/cursors/SpotlightCursor.tsx
"use client";import { useEffect } from "react";import { motion, useMotionValue, useSpring } from "framer-motion";import { usePathname } from "next/navigation";export default function SpotlightCursor({ forceVisible = false }: { forceVisible?: boolean }) {const cursorX = useMotionValue(-100);const cursorY = useMotionValue(-100);const pathname = usePathname();const springConfig = { damping: 20, stiffness: 300, mass: 0.2 }; // Faster trackingconst cursorXSpring = useSpring(cursorX, springConfig);const cursorYSpring = useSpring(cursorY, springConfig);useEffect(() => {if (!forceVisible && pathname !== "/") return;if (typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches) return;const moveCursor = (e: MouseEvent) => {cursorX.set(e.clientX - 400); // Center the 800px lightcursorY.set(e.clientY - 400);};window.addEventListener("mousemove", moveCursor);return () => {window.removeEventListener("mousemove", moveCursor);};}, [cursorX, cursorY, pathname, forceVisible]);if (!forceVisible && pathname !== "/") return null;return (<motion.divclassName="fixed top-0 left-0 w-[800px] h-[800px] rounded-full pointer-events-none z-[9990] hidden md:block"style={{translateX: cursorXSpring,translateY: cursorYSpring,background: "radial-gradient(circle, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 50%)",mixBlendMode: "screen", // Creates a nice 'light' addition}}/>);}
4. Bubble Cursor
Playful particles that float and fade. Great for creative portfolios.
components/cursors/BubbleCursor.tsx
"use client";import { useEffect, useRef } from "react";import { usePathname } from "next/navigation";export default function BubbleCursor({ forceVisible = false }: { forceVisible?: boolean }) {const canvasRef = useRef<HTMLCanvasElement>(null);const pathname = usePathname();useEffect(() => {if (!forceVisible && pathname !== "/") return;if (typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches) return;const canvas = canvasRef.current;if (!canvas) return;const ctx = canvas.getContext("2d");if (!ctx) return;let width = window.innerWidth;let height = window.innerHeight;const resize = () => {width = window.innerWidth;height = window.innerHeight;canvas.width = width;canvas.height = height;};window.addEventListener("resize", resize);resize();const particles: { x: number; y: number; vx: number; vy: number; life: number; size: number }[] = [];const addParticle = (x: number, y: number) => {if (Math.random() > 0.3) return; // limit spawn rateparticles.push({x,y,vx: (Math.random() - 0.5) * 1.5,vy: (Math.random() - 0.5) * 1.5 - 1, // Tend upwardlife: 1,size: Math.random() * 4 + 2});};const onMouseMove = (e: MouseEvent) => {addParticle(e.clientX, e.clientY);};window.addEventListener("mousemove", onMouseMove);const render = () => {ctx.clearRect(0, 0, width, height);for (let i = particles.length - 1; i >= 0; i--) {const p = particles[i];p.x += p.vx;p.y += p.vy;p.life -= 0.015; // Fade rateif (p.life <= 0) {particles.splice(i, 1);continue;}ctx.beginPath();ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);ctx.fillStyle = `rgba(180, 150, 255, ${p.life * 0.5})`;ctx.fill();ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.8})`;ctx.lineWidth = 1;ctx.stroke();}requestAnimationFrame(render);};const animId = requestAnimationFrame(render);return () => {window.removeEventListener("resize", resize);window.removeEventListener("mousemove", onMouseMove);cancelAnimationFrame(animId);};}, [pathname, forceVisible]);if (!forceVisible && pathname !== "/") return null;return (<canvasref={canvasRef}className="fixed inset-0 pointer-events-none z-[9999] hidden md:block"/>);}
5. Elastic Cursor
A spring-loaded follower connected by an elastic line.
components/cursors/ElasticCursor.tsx
"use client";import { useEffect, useState } from "react";import { useSpring, useMotionValue, useTransform, motion } from "framer-motion";import { usePathname } from "next/navigation";export default function ElasticCursor({ forceVisible = false }: { forceVisible?: boolean }) {const pathname = usePathname();const [isHovering, setIsHovering] = useState(false);// Mouse position (instant)const mouseX = useMotionValue(-100);const mouseY = useMotionValue(-100);// Follower position (spring)const springConfig = { damping: 15, stiffness: 150, mass: 0.5 };const followerX = useSpring(mouseX, springConfig);const followerY = useSpring(mouseY, springConfig);// Generate SVG path connecting the two pointsconst linePath = useTransform([mouseX, mouseY, followerX, followerY],([mx, my, fx, fy]: any[]) => `M ${mx} ${my} L ${fx} ${fy}`);useEffect(() => {if (!forceVisible && pathname !== "/") return;if (typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches) return;const moveCursor = (e: MouseEvent) => {mouseX.set(e.clientX);mouseY.set(e.clientY);if(!isHovering) setIsHovering(true);};window.addEventListener("mousemove", moveCursor);return () => window.removeEventListener("mousemove", moveCursor);}, [pathname, forceVisible, mouseX, mouseY, isHovering]);if (!forceVisible && pathname !== "/") return null;return (<>{/* The SVG Line */}<svg className="fixed inset-0 pointer-events-none z-[9998] hidden md:block w-full h-full overflow-visible"><motion.pathd={linePath}stroke="white"strokeWidth={2}strokeOpacity={0.3}fill="none"/></svg>{/* The Follower Dot */}<motion.divclassName="fixed top-0 left-0 w-4 h-4 bg-accent rounded-full pointer-events-none z-[9999] hidden md:block mix-blend-difference"style={{translateX: followerX,translateY: followerY,x: -8, // centery: -8}}/></>);}