Cursor
A custom cursor component with optional spring animations. It can be globally applied to the page or attached specifically to a parent element.
Examples
Cursor with image and spring
Cursor with custom component
Cursor with image and spring
Christian Church, Eastern Europe
Code
'use client';
import React, { useEffect, useState, useRef } from 'react';
import {
motion,
SpringOptions,
useMotionValue,
useSpring,
AnimatePresence,
Transition,
Variant,
} from 'framer-motion';
import { cn } from '@/lib/utils';
type CursorProps = {
children: React.ReactNode;
className?: string;
springConfig?: SpringOptions;
attachToParent?: boolean;
transition?: Transition;
variants?: {
initial: Variant;
animate: Variant;
exit: Variant;
};
onPositionChange?: (x: number, y: number) => void;
};
export function Cursor({
children,
className,
springConfig,
attachToParent,
variants,
transition,
onPositionChange,
}: CursorProps) {
const cursorX = useMotionValue(
typeof window !== 'undefined' ? window.innerWidth / 2 : 0
);
const cursorY = useMotionValue(
typeof window !== 'undefined' ? window.innerHeight / 2 : 0
);
const cursorRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(!attachToParent);
useEffect(() => {
if (!attachToParent) {
document.body.style.cursor = 'none';
} else {
document.body.style.cursor = 'auto';
}
const updatePosition = (e: MouseEvent) => {
cursorX.set(e.clientX);
cursorY.set(e.clientY);
onPositionChange?.(e.clientX, e.clientY);
};
document.addEventListener('mousemove', updatePosition);
return () => {
document.removeEventListener('mousemove', updatePosition);
};
}, [cursorX, cursorY, onPositionChange]);
const cursorXSpring = useSpring(cursorX, springConfig || { duration: 0 });
const cursorYSpring = useSpring(cursorY, springConfig || { duration: 0 });
useEffect(() => {
const handleVisibilityChange = (visible: boolean) => {
setIsVisible(visible);
};
if (attachToParent && cursorRef.current) {
const parent = cursorRef.current.parentElement;
if (parent) {
parent.addEventListener('mouseenter', () => {
parent.style.cursor = 'none';
handleVisibilityChange(true);
});
parent.addEventListener('mouseleave', () => {
parent.style.cursor = 'auto';
handleVisibilityChange(false);
});
}
}
return () => {
if (attachToParent && cursorRef.current) {
const parent = cursorRef.current.parentElement;
if (parent) {
parent.removeEventListener('mouseenter', () => {
parent.style.cursor = 'none';
handleVisibilityChange(true);
});
parent.removeEventListener('mouseleave', () => {
parent.style.cursor = 'auto';
handleVisibilityChange(false);
});
}
}
};
}, [attachToParent]);
return (
<motion.div
ref={cursorRef}
className={cn('pointer-events-none fixed left-0 top-0 z-50', className)}
style={{
x: cursorXSpring,
y: cursorYSpring,
translateX: '-50%',
translateY: '-50%',
}}
>
<AnimatePresence>
{isVisible && (
<motion.div
initial='initial'
animate='animate'
exit='exit'
variants={variants}
transition={transition}
>
{children}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
Please add:
Component API
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | required | Children to be rendered within the custom cursor. Mandatory. |
className | string | Optional CSS class for styling the custom cursor container. | |
springConfig | SpringOptions | Configuration for the spring physics used in the cursor's movement. | |
attachToParent | boolean | false | If true , the cursor will only be visible when hovering over its parent component. |
transition | Transition | Transition settings from framer-motion for animation effects. | |
variants | Object (with initial, animated, exit properties) | Variants for controlling the animation states with specific properties for initial, animate, and exit states. | |
onPositionChange | (position: { x: number, y: number }) => void | Callback function that is called when the cursor position changes. |