- Overview
- How It Works
- Component Architecture
- State Management
- Event Handling
- CSS Architecture
- Performance Optimizations
- Customization Options
- Troubleshooting
Image Cursor Trail ek interactive React component hai jo mouse cursor ke behind images ka trail create karta hai. Ye effect sirf designated container ke andar hi active hota hai.
- Smooth cursor tracking - 60fps smooth movement
- Random image selection - Array se random images pick karta hai
- Boundary detection - Effect sirf container ke andar
- Performance optimized - GPU acceleration aur efficient rendering
- Mobile responsive - Touch devices pe bhi works
- Customizable - Easy to modify aur extend
1. Mouse Move → Track Position
2. Inside Container? → Check Boundaries
3. Add New Image → Random selection + positioning
4. Animate Trail → Fade out + drift effect
5. Cleanup → Remove old images
// 1. Mouse position tracking (document level)
document.addEventListener('mousemove', handleMouseMove);
// 2. Boundary detection
const isInside = (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom);
// 3. Image creation (throttled)
if (now - lastImageTime > 150ms) {
createNewImage();
}
// 4. Animation loop (60fps)
setInterval(() => {
updateTrailImages();
}, 60ms);components/
├── ImageCursorTrail.jsx # Main component logic
├── ImageCursorTrail.css # Styling + animations
└── CarsImagesCollection.jsx # Usage example
const ImageCursorTrail = ({ items, maxNumberOfImages, distance, imgClass, children }) => {
// State management
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isInsideContainer, setIsInsideContainer] = useState(false);
const [imageTrail, setImageTrail] = useState([]);
// Refs for performance
const containerRef = useRef(null);
const lastImageTime = useRef(0);
const imageCounter = useRef(0);
// Event handlers + lifecycle
useEffect(() => { /* Mouse tracking */ }, []);
useEffect(() => { /* Trail animation */ }, []);
useEffect(() => { /* Cleanup */ }, []);
return (
<div ref={containerRef}>
{/* Trail Images */}
{/* Custom Cursor */}
{/* Content */}
</div>
);
};interface Props {
items: string[]; // Array of image URLs
maxNumberOfImages?: number; // Max trail length (default: 5)
distance?: number; // Distance from cursor (default: 25px)
imgClass?: string; // CSS class for image size
children: ReactNode; // Content to display
}const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });- Purpose: Real-time cursor coordinates
- Update: Document-level mousemove event
- Usage: Custom cursor positioning
const [isInsideContainer, setIsInsideContainer] = useState(false);- Purpose: Track if mouse is inside component boundary
- Update: Boundary detection calculation
- Usage: Conditional cursor visibility + trail creation
const [imageTrail, setImageTrail] = useState([]);- Structure:
[
{
id: 1,
x: 500,
y: 300,
src: "image-url",
opacity: 1,
scale: 0.9,
rotation: 15
}
]- Purpose: Trail images data aur positioning
- Update: Add new + animate existing + remove old
const containerRef = useRef(null);- Purpose: DOM reference for boundary calculations
- Usage:
getBoundingClientRect()for position detection
const lastImageTime = useRef(0);- Purpose: Throttling image creation
- Usage: Prevent too many images spawning rapidly
const imageCounter = useRef(0);- Purpose: Unique IDs for images
- Usage: React key prop + z-index management
const handleMouseMove = (e) => {
// Always update position (smooth cursor)
setMousePosition({ x: e.clientX, y: e.clientY });
// Boundary detection
const rect = containerRef.current.getBoundingClientRect();
const isInside = (
e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom
);
setIsInsideContainer(isInside);
// Create trail images (only when inside)
if (isInside && shouldCreateImage()) {
createNewImage(e.clientX, e.clientY);
}
};const createNewImage = (x, y) => {
const newImage = {
id: imageCounter.current++,
x: x,
y: y,
src: items[Math.floor(Math.random() * items.length)], // Random selection
opacity: 1,
scale: 0.8 + Math.random() * 0.4, // Random size (0.8-1.2)
rotation: (Math.random() - 0.5) * 30 // Random rotation (-15° to +15°)
};
setImageTrail(prev => [newImage, ...prev].slice(0, maxNumberOfImages));
};const shouldCreateImage = () => {
const now = Date.now();
if (now - lastImageTime.current > 150) { // 150ms throttle
lastImageTime.current = now;
return true;
}
return false;
};.cursor-trail-container {
position: relative; /* Positioning context */
min-height: 100vh; /* Full viewport */
overflow: hidden; /* Prevent scrollbars */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.cursor-trail-container:hover {
cursor: none; /* Hide default cursor on hover */
}.trail-image {
position: fixed; /* Fixed positioning for smooth movement */
pointer-events: none; /* Don't interfere with mouse events */
object-fit: cover; /* Maintain aspect ratio */
border-radius: 12px; /* Rounded corners */
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); /* Depth */
transition: opacity 0.2s ease-out, transform 0.1s ease-out;
z-index: 40; /* Layer management */
will-change: transform; /* GPU acceleration hint */
}.custom-cursor {
position: fixed;
width: 20px;
height: 20px;
background: radial-gradient(circle, #ffffff 0%, #f0f0f0 70%, #e0e0e0 100%);
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
pointer-events: none;
z-index: 9999; /* Always on top */
transform: translate(-50%, -50%); /* Center on cursor */
transition: all 0.1s ease-out;
will-change: transform; /* Smooth movement */
}// Creation
.trail-image {
animation: imageAppear 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes imageAppear {
0% { opacity: 0; transform: scale(0.5) rotate(-10deg); }
50% { opacity: 0.8; transform: scale(1.1) rotate(5deg); }
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
// Fade out (JavaScript)
setInterval(() => {
setImageTrail(prev =>
prev.map((img, index) => ({
...img,
opacity: Math.max(0, img.opacity - 0.025), // Gradual fade
scale: img.scale * 0.985, // Slight shrink
y: img.y + (index * 1.5) // Drift down
})).filter(img => img.opacity > 0.1) // Remove faded
);
}, 60);.trail-image,
.custom-cursor {
will-change: transform; /* Hint browser for GPU layer */
transform: translateZ(0); /* Force hardware acceleration */
}// Document-level listener (not container-level)
document.addEventListener('mousemove', handleMouseMove);
// Throttling to prevent too many images
if (now - lastImageTime.current > 150) {
createNewImage();
}// Automatic cleanup
.filter(img => img.opacity > 0.1) // Remove faded images
// Limit trail length
.slice(0, maxNumberOfImages) // Cap array size
// Cleanup on unmount
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};// useRef for non-reactive values
const lastImageTime = useRef(0); // No re-renders
const imageCounter = useRef(0); // Persistent counter
// Conditional rendering
{isInsideContainer && <CustomCursor />}<ImageCursorTrail
items={imageUrls} // Array of image URLs
maxNumberOfImages={8} // More/less trail length
distance={40} // Further from cursor
imgClass="large-size" // Custom size class
/>/* Custom size classes */
.large-size { width: 200px; height: 250px; }
.small-size { width: 80px; height: 100px; }
.square-size { width: 150px; height: 150px; }// In component
const THROTTLE_TIME = 100; // Faster image creation
const FADE_SPEED = 0.05; // Faster fade out
const ANIMATION_INTERVAL = 30; // Smoother animation (120fps)/* Different cursor styles */
.glow-cursor {
box-shadow: 0 0 50px rgba(255, 255, 255, 0.8);
background: radial-gradient(circle, #ff6b6b, #4ecdc4);
}
/* Different image effects */
.neon-images {
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
border: 2px solid rgba(0, 255, 255, 0.3);
}Problem: Laggy cursor movement Solution:
// Use document-level listener
document.addEventListener('mousemove', handler);
// Add GPU acceleration
.custom-cursor { will-change: transform; }Problem: No trail images Solution:
// Check image URLs
console.log('Images loaded:', items);
// Check boundary detection
console.log('Inside container:', isInsideContainer);Problem: Trail appears on other pages Solution:
// Proper cleanup
useEffect(() => {
return () => {
document.removeEventListener('mousemove', handleMouseMove);
setImageTrail([]); // Clear trail
};
}, []);Problem: Slow rendering, frame drops Solution:
// Reduce max images
maxNumberOfImages={3}
// Increase throttle time
if (now - lastImageTime.current > 200) { ... }
// Optimize CSS
.trail-image { will-change: transform; }const carImages = ['car1.jpg', 'car2.jpg'];
const animalImages = ['cat.jpg', 'dog.jpg'];
<ImageCursorTrail
items={hoveredCategory === 'cars' ? carImages : animalImages}
/>const [trailConfig, setTrailConfig] = useState({
maxImages: 5,
distance: 25,
size: 'medium'
});
<ImageCursorTrail
maxNumberOfImages={trailConfig.maxImages}
distance={trailConfig.distance}
imgClass={`${trailConfig.size}-size`}
/>function MultiTrailPage() {
return (
<div>
<ImageCursorTrail items={galleryImages}>
<Gallery />
</ImageCursorTrail>
<RegularSection />
<ImageCursorTrail items={portfolioImages}>
<Portfolio />
</ImageCursorTrail>
</div>
);
}- React Hooks - useState, useEffect, useRef
- JavaScript Events - Mouse events, event delegation
- CSS Animations - Transitions, keyframes
- Performance - GPU acceleration, throttling
- Math - Coordinate calculations, boundary detection
- Start with basic mouse tracking
- Add image rendering
- Implement boundary detection
- Add animations and effects
- Optimize performance
- Add customization options
ImageCursorTrail ek advanced interactive component hai jo:
✅ Smooth cursor tracking provide karta hai
✅ Boundary-aware effect sirf designated area main
✅ Performance optimized GPU acceleration ke saath
✅ Highly customizable props aur CSS se
✅ Mobile responsive touch devices ke liye
✅ Production ready proper cleanup aur error handling
Is guide ko follow kar ke aap similar advanced components bana sakte hain! 🚀
import React, { useState, useRef, useEffect } from 'react';Explanation:
useState- For state management (like current image index, dragging status)useRef- To access DOM elements directlyuseEffect- To handle side effects (like autoplay, event listeners)
const [currentIndex, setCurrentIndex] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [isAutoplay, setIsAutoplay] = useState(true);
const [lastMouseX, setLastMouseX] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const containerRef = useRef(null);
const autoplayRef = useRef(null);Purpose of Each State:
currentIndex- Which image is currently showing (0 to 35)isDragging- Whether user is dragging or notisAutoplay- Whether automatic rotation is runninglastMouseX- Previous mouse position (to calculate drag distance)isLoading- Whether image is loadingcontainerRef- Reference to the image containerautoplayRef- Reference to control the autoplay interval
const totalImages = 36; // Total number of images
const autoplaySpeed = 400; // Speed in millisecondsExplanation:
- These variables can be easily modified
- If you have 50 images, just change
totalImages = 50
const getImageUrl = (index) => {
return `/Car/img_0_0_${index + 1}.jpg`;
};Explanation:
- This function returns the correct image path for any index
index + 1because images start from 1, not 0- Example: for index 0, it returns
/Car/img_0_0_1.jpg
useEffect(() => {
if (isAutoplay && !isDragging) {
autoplayRef.current = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % totalImages);
}, autoplaySpeed);
} else {
clearInterval(autoplayRef.current);
}
return () => clearInterval(autoplayRef.current);
}, [isAutoplay, isDragging, autoplaySpeed, totalImages]);Step-by-step breakdown:
- If autoplay is ON and user is not dragging
- Start a timer that runs every
autoplaySpeedmilliseconds - In the timer, increment the
currentIndex % totalImagesensures that after the last image, it goes back to the first- If autoplay is OFF or user is dragging, stop the timer
- Clean up the timer when component unmounts
const handleMouseDown = (e) => {
setIsDragging(true);
setIsAutoplay(false);
setLastMouseX(e.clientX);
};Explanation:
- When user presses mouse, start dragging
- Pause autoplay
- Store current mouse X position
const handleMouseMove = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - lastMouseX;
const sensitivity = 5;
if (Math.abs(deltaX) > sensitivity) {
const direction = deltaX > 0 ? 1 : -1;
setCurrentIndex((prev) => {
let newIndex = prev + direction;
if (newIndex < 0) newIndex = totalImages - 1;
if (newIndex >= totalImages) newIndex = 0;
return newIndex;
});
setLastMouseX(e.clientX);
}
};Step-by-step:
- If not dragging, do nothing
- Calculate mouse movement distance (
deltaX) - Only change image if movement is greater than sensitivity
- Right movement = next image, Left movement = previous image
- Handle edge cases (wrap around for first/last image)
const handleMouseUp = () => {
setIsDragging(false);
};Explanation:
- When mouse is released, stop dragging
const handleTouchStart = (e) => {
setIsDragging(true);
setIsAutoplay(false);
setLastMouseX(e.touches[0].clientX);
};
const handleTouchMove = (e) => {
// Similar logic as handleMouseMove but uses touches[0].clientX
};
const handleTouchEnd = () => {
setIsDragging(false);
};Explanation:
- Touch events are for mobile devices
- Logic is same as mouse events, but uses
touches[0].clientXinstead ofclientX
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', handleTouchEnd);
}
return () => {
// Remove all event listeners
};
}, [isDragging, lastMouseX]);Explanation:
- Add event listeners to the document level
- This allows dragging to work even when mouse moves outside the image container
- Cleanup function removes all listeners to prevent memory leaks
<div
ref={containerRef}
className={`viewer-container ${isDragging ? 'dragging' : ''}`}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
>
<img
src={getImageUrl(currentIndex)}
alt={`360° view frame ${currentIndex + 1}`}
className="viewer-image"
draggable={false}
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
{isLoading && (
<div className="loading-overlay">
<div className="loading-text">Loading...</div>
</div>
)}
</div>Key Points:
getImageUrl(currentIndex)gets the current image pathdraggable={false}disables browser's default drag behavior- Loading overlay only shows when
isLoadingis true onLoadandonErrorboth set loading to false
<div className="controls">
<div className="nav-buttons">
<button onClick={() => setCurrentIndex(prev => prev === 0 ? totalImages - 1 : prev - 1)}>
← Prev
</button>
<button onClick={() => setCurrentIndex(prev => (prev + 1) % totalImages)}>
Next →
</button>
</div>
<div className="autoplay-controls">
<button onClick={() => setIsAutoplay(!isAutoplay)}>
{isAutoplay ? 'Pause' : 'Play'}
</button>
</div>
</div>Explanation:
- Prev button: If index is 0, jump to last image, otherwise subtract 1
- Next button: Use modulo operator to wrap around
- Autoplay button: Toggle the current autoplay state
<div className="progress-section">
<div className="progress-info">
<span>Frame {currentIndex + 1} of {totalImages}</span>
<span>{Math.round((currentIndex / (totalImages - 1)) * 100)}%</span>
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(currentIndex / (totalImages - 1)) * 100}%` }}
/>
</div>
</div>Explanation:
- Calculate progress percentage based on current index
- Progress bar width changes dynamically using inline styles
- Shows both frame number and percentage
const Viewer360 = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const totalImages = 36;
return (
<div>
<img src={`/images/img_${currentIndex + 1}.jpg`} />
</div>
);
};<button onClick={() => setCurrentIndex(prev => (prev + 1) % totalImages)}>
Next
</button>const [isDragging, setIsDragging] = useState(false);
const [lastMouseX, setLastMouseX] = useState(0);
const handleMouseDown = (e) => {
setIsDragging(true);
setLastMouseX(e.clientX);
};useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % totalImages);
}, 400);
return () => clearInterval(interval);
}, []);- State Management: How React tracks changing data
- Event Handling: Responding to user interactions
- useEffect: Managing side effects and cleanup
- Modulo Operator (%): For wrapping around arrays
- Event Listeners: Document-level event handling
- Conditional Rendering: Showing/hiding elements based on state
This structure allows you to create any 360° viewer by just changing the image paths and total count!