Learn by building! See the demo, understand the theory, copy the code.
📱 Mobile Tip: This tutorial works best on desktop for full interactive experience!
This is what we'll build! Move your mouse and watch the magic happen.
⚡ How to Use This Demo:
Click the step buttons above (Mouse, Hover, Follower, Themes, Trail) to progressively activate each feature. Then scroll down to read the explanation and code for that step!
💡 Start with "Mouse" to see the first feature, then move through each step to build up the full effect.
The Foundation: Everything starts with knowing where the user's mouse is! We need to track the cursor position so our elements can react to it.
Browsers fire a mousemove event every time the mouse moves. We listen to this event and save the X and Y coordinates.
Imagine the screen is a giant graph paper. When you move your mouse:
import { useState, useEffect } from 'react';
function MouseTracker() {
// Create a state to store mouse position
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
useEffect(() => {
// This function runs every time the mouse moves
const handleMouseMove = (e) => {
setMousePos({
x: e.clientX, // Horizontal position
y: e.clientY // Vertical position
});
};
// Tell the browser to listen for mouse movement
window.addEventListener('mousemove', handleMouseMove);
// Cleanup: Remove listener when component unmounts
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty array means "run once on mount"
return (
<div>
<p>Mouse position: ({mousePos.x}, {mousePos.y})</p>
</div>
);
}Making It Interactive: Now let's make the bricks react when you hover over them! They should grow bigger, change color, and rotate slightly.
We track which brick is being hovered and apply special styles to it. CSS handles the smooth animation automatically using transition.
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)function InteractiveBricks() {
const [hoveredBrick, setHoveredBrick] = useState(null);
// Create array of bricks [0, 1, 2, 3, ..., 23]
const bricks = Array.from({ length: 24 }, (_, i) => i);
const getBrickStyle = (index) => {
const isHovered = hoveredBrick === index;
return {
width: isHovered ? '70px' : '60px', // Grow when hovered
height: isHovered ? '70px' : '60px',
backgroundColor: isHovered ? '#FFD700' : '#8B4513', // Gold vs Brown
transform: isHovered
? 'scale(1.15) rotate(5deg)' // Bigger + slight rotation
: 'scale(1) rotate(0deg)', // Normal state
transition: 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)', // Bouncy!
cursor: 'pointer',
};
};
return (
<div>
{bricks.map((brick) => (
<div
key={brick}
style={getBrickStyle(brick)}
onMouseEnter={() => setHoveredBrick(brick)} // Track entry
onMouseLeave={() => setHoveredBrick(null)} // Track exit
>
{brick + 1}
</div>
))}
</div>
);
}The Star of the Show: Let's add a butterfly that smoothly follows your cursor! Instead of jumping instantly, it gracefully glides to catch up.
Lerp is a technique that smoothly transitions from one value to another. Instead of teleporting to the target, we move a percentage of the distance each frame.
newPosition = currentPosition + (targetPosition - currentPosition) × speed Example with speed = 0.1 (10% per frame): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Frame 0: Position = 0, Target = 100 Frame 1: Position = 0 + (100-0) × 0.1 = 10 Frame 2: Position = 10 + (100-10) × 0.1 = 19 Frame 3: Position = 19 + (100-19) × 0.1 = 27.1 ...keeps getting closer!
function ButterflyFollower() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [butterflyPos, setButterflyPos] = useState({ x: 0, y: 0 });
const animationFrameRef = useRef(null);
// Track mouse (from Step 1)
useEffect(() => {
const handleMouseMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Smooth following animation
useEffect(() => {
const animate = () => {
setButterflyPos((prev) => {
const speed = 0.1; // Move 10% of the distance per frame
return {
// Lerp formula for X coordinate
x: prev.x + (mousePos.x - prev.x) * speed,
// Lerp formula for Y coordinate
y: prev.y + (mousePos.y - prev.y) * speed,
};
});
// Request next frame (60 times per second)
animationFrameRef.current = requestAnimationFrame(animate);
};
// Start the animation loop
animationFrameRef.current = requestAnimationFrame(animate);
// Cleanup: Stop animation when component unmounts
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [mousePos]); // Re-run when mouse position changes
return (
<div
style={{
position: 'fixed',
left: butterflyPos.x,
top: butterflyPos.y,
transform: 'translate(-50%, -50%)', // Center on position
pointerEvents: 'none', // Don't block mouse clicks
fontSize: '40px',
}}
>
🦋
</div>
);
}Multiple Personalities: Let's add the ability to switch between gaming themes! Minecraft style (blocky, brown) or Roblox style (smooth, colorful).
We create a theme object that stores all the visual properties, then swap between them. This keeps our code organized and makes adding new themes super easy!
function ThemedDemo() {
const [theme, setTheme] = useState('minecraft');
// Theme configuration object
const themes = {
minecraft: {
name: 'Minecraft',
brickColors: ['#8B4513', '#A0522D', '#CD853F', '#DEB887'],
butterflyEmoji: '🦋',
backgroundColor: '#87CEEB', // Sky blue
borderRadius: '2px', // Sharp edges
borderStyle: '2px solid #654321',
},
roblox: {
name: 'Roblox',
brickColors: ['#FF0000', '#00FF00', '#0000FF', '#FFFF00'],
butterflyEmoji: '🚀',
backgroundColor: '#A8DADC', // Soft blue
borderRadius: '8px', // Rounded edges
borderStyle: '2px solid rgba(255,255,255,0.5)',
},
};
// Get current theme
const currentTheme = themes[theme];
return (
<div style={{ backgroundColor: currentTheme.backgroundColor }}>
{/* Theme Switcher Buttons */}
<button onClick={() => setTheme('minecraft')}>
⛏️ Minecraft
</button>
<button onClick={() => setTheme('roblox')}>
🎮 Roblox
</button>
{/* Bricks using current theme */}
<div style={{
backgroundColor: currentTheme.brickColors[0],
borderRadius: currentTheme.borderRadius,
border: currentTheme.borderStyle,
}}>
Brick
</div>
{/* Follower using current theme emoji */}
<div>{currentTheme.butterflyEmoji}</div>
</div>
);
}Want to add a Super Mario theme? Just add to the themes object:
mario: {
name: 'Super Mario',
brickColors: ['#C84C09', '#FCE76D'],
butterflyEmoji: '🍄',
backgroundColor: '#5C94FC',
}The Finishing Touch: Let's add a magical sparkle trail behind the butterfly! It will leave a fading trail of ✨ as it moves.
We store an array of recent positions and render sparkles at each position. Older sparkles fade out by reducing their opacity based on their age.
function TrailEffect() {
const [butterflyPos, setButterflyPos] = useState({ x: 0, y: 0 });
const [trail, setTrail] = useState([]);
// Add to trail every 60 milliseconds
useEffect(() => {
const interval = setInterval(() => {
setTrail((prev) => {
// Keep only last 8 positions (prevent memory issues)
const newTrail = [...prev.slice(-8)];
// Add current position with unique ID
newTrail.push({
x: butterflyPos.x,
y: butterflyPos.y,
id: Date.now(), // Unique identifier
});
return newTrail;
});
}, 60); // 60ms = ~16 times per second
return () => clearInterval(interval);
}, [butterflyPos]);
return (
<div>
{/* Butterfly */}
<div
style={{
position: 'fixed',
left: butterflyPos.x,
top: butterflyPos.y,
}}
>
🦋
</div>
{/* Trail sparkles */}
{trail.map((point, index) => (
<div
key={point.id}
style={{
position: 'fixed',
left: point.x,
top: point.y,
fontSize: '14px',
// Fade based on position in array
// Newer sparkles (higher index) = more visible
opacity: (index / trail.length) * 0.5,
pointerEvents: 'none', // Don't block clicks
}}
>
✨
</div>
))}
</div>
);
}(index / trail.length) × 0.5This creates a natural fade-out effect!
You've learned how to build interactive mouse effects from scratch!
Foundation of all interactions
Lerp & requestAnimationFrame
Easy to customize & extend
// Save as: components/InteractiveMouseEffects.jsx
import { useState, useEffect, useRef } from 'react';
export default function InteractiveMouseEffects() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [hoveredBrick, setHoveredBrick] = useState(null);
const [butterflyPos, setButterflyPos] = useState({ x: 0, y: 0 });
const [trail, setTrail] = useState([]);
const [theme, setTheme] = useState('minecraft');
const animationFrameRef = useRef(null);
// Mouse tracking
useEffect(() => {
const handleMouseMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Butterfly following
useEffect(() => {
const animate = () => {
setButterflyPos((prev) => ({
x: prev.x + (mousePos.x - prev.x) * 0.1,
y: prev.y + (mousePos.y - prev.y) * 0.1,
}));
animationFrameRef.current = requestAnimationFrame(animate);
};
animationFrameRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationFrameRef.current);
}, [mousePos]);
// Trail effect
useEffect(() => {
const interval = setInterval(() => {
setTrail((prev) => [
...prev.slice(-8),
{ x: butterflyPos.x, y: butterflyPos.y, id: Date.now() },
]);
}, 60);
return () => clearInterval(interval);
}, [butterflyPos]);
const themes = {
minecraft: {
colors: ['#8B4513', '#A0522D', '#CD853F'],
emoji: '🦋',
bg: '#87CEEB',
},
roblox: {
colors: ['#FF0000', '#00FF00', '#0000FF'],
emoji: '🚀',
bg: '#A8DADC',
},
};
const current = themes[theme];
return (
<div style={{ background: current.bg, minHeight: '100vh' }}>
{/* Your awesome interactive component! */}
</div>
);
}💬 Share this tutorial:

Software Engineer & Cloud Solutions Consultant specializing in React, Next.js, and interactive web experiences.