Docs
Follow Cursor Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file follow-cursor-effect.tsx
inside the components/mage-ui/cursor-effects
directory.
mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/follow-cursor-effect.tsx
Paste the code
Open the newly created file and paste the following code:
'use client';
import React, { useEffect } from 'react';
interface FollowCursorProps {
color?: string;
}
const FollowCursor: React.FC<FollowCursorProps> = ({ color = '#323232a6' }) => {
useEffect(() => {
let canvas: HTMLCanvasElement;
let context: CanvasRenderingContext2D | null;
let animationFrame: number;
let width = window.innerWidth;
let height = window.innerHeight;
let cursor = { x: width / 2, y: height / 2 };
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
class Dot {
position: { x: number; y: number };
width: number;
lag: number;
constructor(x: number, y: number, width: number, lag: number) {
this.position = { x, y };
this.width = width;
this.lag = lag;
}
moveTowards(x: number, y: number, context: CanvasRenderingContext2D) {
this.position.x += (x - this.position.x) / this.lag;
this.position.y += (y - this.position.y) / this.lag;
context.fillStyle = color;
context.beginPath();
context.arc(
this.position.x,
this.position.y,
this.width,
0,
2 * Math.PI
);
context.fill();
context.closePath();
}
}
const dot = new Dot(width / 2, height / 2, 10, 10);
const onMouseMove = (e: MouseEvent) => {
cursor.x = e.clientX;
cursor.y = e.clientY;
};
const onWindowResize = () => {
width = window.innerWidth;
height = window.innerHeight;
if (canvas) {
canvas.width = width;
canvas.height = height;
}
};
const updateDot = () => {
if (context) {
context.clearRect(0, 0, width, height);
dot.moveTowards(cursor.x, cursor.y, context);
}
};
const loop = () => {
updateDot();
animationFrame = requestAnimationFrame(loop);
};
const init = () => {
if (prefersReducedMotion.matches) {
console.log('Reduced motion enabled, cursor effect skipped.');
return;
}
canvas = document.createElement('canvas');
context = canvas.getContext('2d');
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.pointerEvents = 'none';
canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('resize', onWindowResize);
loop();
};
const destroy = () => {
if (canvas) canvas.remove();
cancelAnimationFrame(animationFrame);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('resize', onWindowResize);
};
prefersReducedMotion.onchange = () => {
if (prefersReducedMotion.matches) {
destroy();
} else {
init();
}
};
init();
return () => {
destroy();
};
}, [color]);
// Adding a wrapper div with h-screen Tailwind class to ensure full height
return <div className="h-screen">{null}</div>;
};
export default FollowCursor;