Docs
Glitch 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 glitch-cursor-effect.tsx
inside the components/mage-ui/cursor-effects
directory.
mkdir -p components/mage-ui/cursor-effects && touch components/mage-ui/cursor-effects/glitch-cursor-effect.tsx
Paste the code
Open the newly created file and paste the following code:
'use client';
import React, { useState, useEffect, useRef, useLayoutEffect, type RefObject } from 'react';
// Mouse hook interface
interface MouseState {
x: number | null;
y: number | null;
elementX: number | null;
elementY: number | null;
elementPositionX: number | null;
elementPositionY: number | null;
}
// useMouse hook implementation
function useMouse(): [MouseState, RefObject<HTMLDivElement>] {
const [state, setState] = useState<MouseState>({
x: null,
y: null,
elementX: null,
elementY: null,
elementPositionX: null,
elementPositionY: null,
});
const ref = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
const newState: Partial<MouseState> = {
x: event.pageX,
y: event.pageY,
};
if (ref.current instanceof Element) {
const { left, top } = ref.current.getBoundingClientRect();
const elementPositionX = left + window.scrollX;
const elementPositionY = top + window.scrollY;
const elementX = event.pageX - elementPositionX;
const elementY = event.pageY - elementPositionY;
newState.elementX = elementX;
newState.elementY = elementY;
newState.elementPositionX = elementPositionX;
newState.elementPositionY = elementPositionY;
}
setState((s) => ({
...s,
...newState,
}));
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return [state, ref];
}
// Interface for glitch offset
interface GlitchOffset {
x: number;
y: number;
scale: number;
rotation: number;
opacity: number;
hue: number;
}
// GlitchCursor component
const GlitchCursor: React.FC = () => {
const [mouseState, ref] = useMouse();
const [glitchActive, setGlitchActive] = useState<boolean>(false);
const [glitchOffsets, setGlitchOffsets] = useState<GlitchOffset[]>([]);
const [intensity, setIntensity] = useState<number>(1);
useEffect(() => {
let interval: NodeJS.Timeout | undefined;
if (glitchActive) {
interval = setInterval(() => {
const newOffsets: GlitchOffset[] = Array(5)
.fill(0)
.map(() => ({
x: (Math.random() - 0.5) * 20 * intensity,
y: (Math.random() - 0.5) * 20 * intensity,
scale: 0.8 + Math.random() * 0.4,
rotation: (Math.random() - 0.5) * 45 * intensity,
opacity: 0.5 + Math.random() * 0.5,
hue: Math.random() * 360,
}));
setGlitchOffsets(newOffsets);
}, 50);
} else {
setGlitchOffsets([]);
}
return () => {
if (interval) clearInterval(interval);
};
}, [glitchActive, intensity]);
const handleMouseSpeed = (e: React.MouseEvent): void => {
const speed = Math.sqrt(
Math.pow(e.movementX, 2) + Math.pow(e.movementY, 2)
);
setIntensity(Math.min(Math.max(speed / 10, 1), 3));
};
// Extract mouse position with null safety
const mouseX = mouseState.x !== null ? mouseState.x : 0;
const mouseY = mouseState.y !== null ? mouseState.y : 0;
return (
<div
className="relative w-full h-full"
ref={ref}
onMouseMove={handleMouseSpeed}
>
{mouseState.x !== null && mouseState.y !== null && (
<>
{/* Glitch layers */}
{glitchOffsets.map((offset, index) => (
<div
key={index}
className="fixed pointer-events-none mix-blend-screen"
style={{
left: `${mouseX + offset.x}px`,
top: `${mouseY + offset.y}px`,
transform: `translate(-50%, -50%)
scale(${offset.scale})
rotate(${offset.rotation}deg)`,
opacity: offset.opacity,
}}
>
<div
className="w-8 h-8 rounded-full"
style={{
background: `hsl(${offset.hue}, 100%, 50%)`,
filter: 'blur(2px)',
}}
/>
</div>
))}
{/* Main cursor */}
<div
className="fixed pointer-events-none z-50"
style={{
left: `${mouseX}px`,
top: `${mouseY}px`,
transform: 'translate(-50%, -50%)',
}}
>
<div className="w-8 h-8 bg-white rounded-full mix-blend-screen" />
</div>
{/* Static effect overlay */}
{glitchActive && (
<div
className="fixed pointer-events-none mix-blend-screen"
style={{
left: `${mouseX}px`,
top: `${mouseY}px`,
transform: 'translate(-50%, -50%)',
width: '100px',
height: '100px',
background: `url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E")`,
opacity: 0.3,
}}
/>
)}
</>
)}
<div className="flex flex-col items-center justify-center h-full gap-8">
<button
className={`px-8 py-4 bg-red-600/30 text-white rounded-lg transition-all duration-300 relative overflow-hidden
${glitchActive ? 'animate-pulse' : ''}`}
onMouseEnter={() => setGlitchActive(true)}
onMouseLeave={() => setGlitchActive(false)}
>
Trigger Glitch
{glitchActive && (
<div
className="absolute inset-0 bg-red-500/20"
style={{
animation: 'glitchOverlay 0.3s infinite'
}}
/>
)}
</button>
</div>
<style jsx global>{`
@keyframes glitchOverlay {
0% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
`}</style>
</div>
);
};
// Page component
const GlitchCursorPage: React.FC = () => {
return (
<div className="w-screen h-screen">
<GlitchCursor />
</div>
);
};
export default GlitchCursorPage;