How would you implement a carousel/slider component in React?
Advertisement
🧩 Scenario
🧠 Architecture Walkthrough
The Clone Strategy for Seamless Infinite Looping
A carousel with five slides that wraps from slide 5 back to slide 1 appears to jump backwards across four slides if you simply reset the index to 0. The clone strategy solves this by creating an extended slide array: clone the last slide and prepend it, clone the first slide and append it. The real slides live at indices 1 through 5 in this extended array, the initial currentIndex is 1 (pointing at the first real slide), and when the user navigates past index 6 (the appended clone of slide 1), you detect that boundary and perform a silent index jump back to 1. Because the clone of slide 1 at index 6 looks identical to the real slide 1 at index 1, the jump is visually indistinguishable if you disable the CSS transition for that one frame. The same logic runs in reverse at the other end: reaching index 0 (the prepended clone of slide 5) triggers a silent jump to index 5. This technique is widely used in production carousels precisely because it requires no special-casing in the navigation logic — nextSlide and prevSlide are always just +1 and -1.
Disabling Transition for the Invisible Jump
The jump from a boundary clone back to the real slide must happen without the user seeing movement. The demo handles this by setting isTransitioning to false before changing currentIndex, then re-enabling it with a 50ms timeout. During the frame where isTransitioning is false, the CSS transition property is removed from the slides container: transition: isTransitioning && !isDragging ? 'transform 0.3s ease-in-out' : 'none'. The 50ms delay before re-enabling is longer than a single frame (which is ~16ms) but short enough to be imperceptible. Setting the timeout too short can cause a race where the transition re-enables before the DOM has committed the new index position, making the jump visible. This is one of those timing details that requires testing across low-end devices — on a fast machine the 50ms always works, but on a slow device under load, you may need to use requestAnimationFrame callbacks instead of setTimeout for more reliable frame synchronization.
Coordinating Auto-Play, Hover, and Drag Without Race Conditions
The auto-play effect depends on two state variables: isAutoPlaying and isDragging. When either is false, the interval is cleared. The useEffect for auto-play has both in its dependency array, so changing either one immediately re-evaluates whether to start or stop the interval. This means starting a drag clears the current timer and prevents a new one from starting until isDragging returns to false — no mid-drag slide jumps are possible. Hovering over the carousel sets isAutoPlaying to false via onMouseEnter, which has the same clearing effect. One subtlety is that nextSlide is called inside the setInterval callback, but nextSlide itself is defined with setCurrentIndex(prev => prev + 1) — the functional updater form. This is important because nextSlide captured in the interval closure would otherwise reference a stale currentIndex value from the render when the interval was created. Using the functional updater bypasses the closure entirely and always operates on the current state.
💡 Key Code Explained
// Create extended slides array with clones for infinite loop
const extendedSlides = [
SLIDES[SLIDES.length - 1], // Clone of last slide
...SLIDES,
SLIDES[0], // Clone of first slide
];
// Handle infinite loop jumps
useEffect(() => {
if (currentIndex === 0) {
// At clone of last slide, jump to real last slide
setTimeout(() => {
setIsTransitioning(false);
setCurrentIndex(SLIDES.length);
}, 300);
} else if (currentIndex === extendedSlides.length - 1) {
// At clone of first slide, jump to real first slide
setTimeout(() => {
setIsTransitioning(false);
setCurrentIndex(1);
}, 300);
}
// Re-enable transition after jump
const timer = setTimeout(() => {
setIsTransitioning(true);
}, 50);
return () => clearTimeout(timer);
}, [currentIndex, extendedSlides.length]);
The 300ms delay before the jump matches the CSS transition duration of 0.3s. This gives the animation time to complete — the user sees the carousel animate to the clone position — before the index silently jumps to the real position. The jump itself happens with setIsTransitioning(false) fired first, then setCurrentIndex(SLIDES.length). Since both are called synchronously in the same setTimeout callback, React batches them into a single render where the transition is already disabled when the new index is applied, so no animation plays for the jump. The re-enable timeout of 50ms fires after the jump render has committed. One thing to note: extendedSlides.length is in the dependency array, but it never changes — it is SLIDES.length + 2 and SLIDES is a constant. Including it is technically correct but adds no meaningful reactivity; a lint rule would flag it as an unnecessary dependency.
const translateX =
-currentIndex * slideWidth +
(isDragging ? dragCurrentX.current - dragStartX.current : 0);
This single line merges the index-based position with the live drag offset. currentIndex * slideWidth converts the slide index to pixels — with slideWidth fixed at 300, slide 1 sits at -300px, slide 2 at -600px, and so on. During a drag, dragCurrentX.current - dragStartX.current is the number of pixels the user has moved since touch/mousedown. Adding this offset to the base position makes the slides follow the finger in real time without updating currentIndex (which would re-trigger the loop-detection effect on every pixel moved). When isDragging is false, the offset term is zero and the position snaps to the clean index-based value. The refs for drag positions are used instead of state because updating state on every mousemove event would trigger a re-render per pixel, causing performance issues on complex slide content.
⚖️ Tradeoffs
| Approach | Pro | Con |
|---|---|---|
| Clone-based infinite loop (chosen) | Works with any CSS transition, no visible seam | Requires careful timing for the silent jump; dot indicator index calculation needs adjustment |
| CSS scroll-snap | Native browser behavior, no JS timing issues | Limited animation control, harder to implement drag gesture with custom thresholds |
| Modulo-based index wrapping | No clones needed, simpler index math | Transition from last to first slide animates backwards through all intermediate slides unless you manually detect the wrap direction |
🎯 What Interviewers Actually Check
- Explains the clone strategy and silent jump mechanism rather than just saying "use modulo for index wrapping"
- Uses refs for drag position tracking (
dragStartX,dragCurrentX) rather than state, and can explain why state would cause performance issues - Knows that
setCurrentIndex(prev => prev + 1)inside asetIntervalis necessary to avoid stale closures — not just that it works, but why - Describes the timing relationship between the 300ms jump delay and the CSS transition duration
- Notes that dot indicators need a separate derivation from
currentIndexbecausecurrentIndexincludes the offset for the prepended clone
❓ Follow-Up Questions
- If the carousel contains images, the first drag interaction can feel laggy because images fire their own drag events. How would you prevent this without adding
user-select: noneto the entire page? - The current auto-play uses
setIntervalwhich can drift over time. How would you implement a drift-free auto-advance usingrequestAnimationFrameand timestamps? - How would you make the carousel accessible — specifically, how would you announce slide changes to screen readers without a visual announcement?
- The
slideWidthis hardcoded at 300px. How would you make this responsive using aResizeObserverto measure the container's actual width at runtime? - Your design team wants a "peek" mode where the next and previous slides are partially visible on the sides. What changes to
translateX,overflow, and the container width does this require?
🎮 Live Demo
📝 Summary
A production carousel is built on two core techniques: the clone strategy for seamless infinite looping, and the careful coordination of CSS transition state with the silent index jump. Using refs instead of state for drag position tracking is not a style preference — it is a performance requirement, since state updates trigger re-renders and mousemove fires up to 60 times per second. The functional updater form of setCurrentIndex in the auto-play interval is equally important, preventing stale closure bugs that would otherwise cause the carousel to lose track of its position after the first interval fires. These details separate a carousel that works in isolation from one that remains stable under concurrent user interactions, auto-play, and rapid navigation.
Should I use CSS scroll snapping or manual translateX animations?
Scroll snap is simpler but offers less control. translateX + JS gives full custom behavior.
How do you implement infinite looping?
Clone first and last slides and jump index without animation when reaching edges.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement