Build a Image Carousel
State = current index. Prev/next wrap or clamp; dots/thumbnails jump. Add: autoplay with pause-on-hover, keyboard arrows, swipe on touch, lazy-loading images, and accessibility (aria-roledescription, live region, focus management). Use transform for the slide animation.
A carousel is simple state — currentIndex — but interviewers grade autoplay hygiene, accessibility, and the interaction surface.
Core implementation
function Carousel({ images, autoPlay = true, interval = 4000 }) {
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const go = (i) => setIndex((i + images.length) % images.length); // wrap
const next = () => go(index + 1);
const prev = () => go(index - 1);
// autoplay — pause on hover/focus, clean up the timer
useEffect(() => {
if (!autoPlay || paused) return;
const id = setInterval(next, interval);
return () => clearInterval(id);
}, [autoPlay, paused, index, interval]);
return (
<div
role="region" aria-roledescription="carousel" aria-label="Gallery"
onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
onKeyDown={(e) => {
if (e.key === "ArrowRight") next();
if (e.key === "ArrowLeft") prev();
}}
>
<div style={{ transform: `translateX(-${index * 100}%)`, transition: "transform .3s" }}>
{images.map((src, i) => (
<img key={src} src={src} loading={i === index ? "eager" : "lazy"}
aria-hidden={i !== index} alt={`Slide ${i + 1}`} />
))}
</div>
<button onClick={prev} aria-label="Previous slide">‹</button>
<button onClick={next} aria-label="Next slide">›</button>
<div role="tablist">
{images.map((_, i) => (
<button key={i} aria-label={`Go to slide ${i + 1}`}
aria-selected={i === index} onClick={() => go(i)} />
))}
</div>
</div>
);
}What's being graded
- Index management — wrap (
% length) or clamp; dots jump directly. - Autoplay hygiene —
setIntervalin an effect with cleanup; pause on hover and focus (and ideally when the tab is hidden /prefers-reduced-motion). An autoplay carousel you can't pause is an accessibility failure. - Slide animation — animate
transform: translateX(composite-only, GPU, smooth), notleft. - Keyboard support — arrow keys.
- Touch / swipe — pointer events tracking drag distance on touch devices.
- Lazy-loading — only eager-load the visible (and maybe adjacent) images;
loading="lazy"the rest. - Accessibility —
aria-roledescription="carousel", label the region, label the prev/next/dot buttons,aria-hiddennon-visible slides, and anaria-liveregion announcing "slide X of N." Respectprefers-reduced-motion.
The framing
"State is just currentIndex; prev/next wrap with modulo, dots jump. The grading is in the interaction and a11y: autoplay must be a cleaned-up setInterval in an effect that pauses on hover and focus — an unpausable autoplay carousel fails accessibility. Animate transform: translateX for smooth GPU-composited sliding. Add arrow-key support and touch swipe via pointer events. Lazy-load all but the visible image. And the ARIA: aria-roledescription=carousel, labeled controls, aria-hidden on offscreen slides, an aria-live 'slide X of N', and respect prefers-reduced-motion."
Follow-up questions
- •Why must autoplay pause on hover/focus?
- •Why animate transform instead of left?
- •How do you make a carousel accessible?
- •How would you add touch/swipe support?
Common mistakes
- •Autoplay with no pause-on-hover/focus, or no timer cleanup.
- •Animating left/margin instead of transform.
- •Eager-loading every image upfront.
- •No keyboard support, no ARIA, ignoring prefers-reduced-motion.
- •Off-by-one in wrap-around index math.
Performance considerations
- •transform-based sliding is composite-only (GPU, 60fps). Lazy-loading offscreen images cuts initial bytes. Clean up the autoplay timer to avoid leaks and setState-after-unmount.
Edge cases
- •Single image (hide controls).
- •Wrapping from last to first.
- •Tab backgrounded during autoplay.
- •Very wide/tall images, varying aspect ratios.
- •Rapid prev/next clicks mid-animation.
Real-world examples
- •Product image galleries, hero banners, onboarding slides.
- •Embla/Swiper libraries implementing exactly these concerns.