Machine-Coding: Build an Image Carousel

The image carousel machine-coding round is one of the more deceptive frontend interview tests. Surface-simple — show images, navigate between them — the round actually tests touch handling, animation, autoplay management, accessibility for a notoriously a11y-hostile component, and image performance. Senior interviewers grade many dimensions; junior candidates often nail the visual but miss the rest.

This piece walks through the implementation patterns and what interviewers grade.

The typical prompt

  • “Build an image carousel that cycles through 5 images. The user can advance with arrow buttons.”
  • “Build a swipe-able image gallery for mobile.”
  • “Implement an autoplay carousel that pauses on hover and respects prefers-reduced-motion.”

What interviewers grade

  • Working navigation. Previous and next buttons advance the carousel.
  • Touch / swipe. On touch devices, swipe gestures advance.
  • Animation. Smooth slide transitions, not abrupt swaps.
  • Indicators. Dots or pagination showing current position.
  • Autoplay. Auto-advance after N seconds, pause on hover and focus.
  • Accessibility. Keyboard support, ARIA live region for slide changes, reduced-motion support.
  • Image loading. Lazy-load off-screen images, handle loading states.
  • Edge cases. Wrap-around (last → first), single-image case, empty state.

Implementation

Step 1: state and structure

function Carousel({ images }) {
  const [current, setCurrent] = useState(0);

  function next() {
    setCurrent(c => (c + 1) % images.length);
  }
  function prev() {
    setCurrent(c => (c - 1 + images.length) % images.length);
  }

  return (
    <div className="carousel" role="region" aria-roledescription="carousel">
      <div className="slides" style={{ transform: `translateX(-${current * 100}%)` }}>
        {images.map((src, i) => (
          <img key={src} src={src} alt={`Slide ${i + 1}`} loading={Math.abs(i - current) <= 1 ? 'eager' : 'lazy'} />
        ))}
      </div>
      <button onClick={prev} aria-label="Previous slide">←</button>
      <button onClick={next} aria-label="Next slide">→</button>
      <div className="indicators">
        {images.map((_, i) => (
          <button
            key={i}
            onClick={() => setCurrent(i)}
            aria-label={`Go to slide ${i + 1}`}
            aria-current={i === current}
          />
        ))}
      </div>
    </div>
  );
}

The transform-based positioning is performance-friendly (composite-only). The lazy-loading hints reduce initial network load.

Step 2: keyboard support

useEffect(() => {
  function handleKeyDown(e) {
    if (e.key === 'ArrowLeft') prev();
    else if (e.key === 'ArrowRight') next();
  }
  document.addEventListener('keydown', handleKeyDown);
  return () => document.removeEventListener('keydown', handleKeyDown);
}, []);

For senior+ rounds, scope this to when the carousel is focused, not globally.

Step 3: touch / swipe support

const touchStartRef = useRef<number>(0);

function onTouchStart(e) {
  touchStartRef.current = e.touches[0].clientX;
}

function onTouchEnd(e) {
  const delta = e.changedTouches[0].clientX - touchStartRef.current;
  if (Math.abs(delta) > 50) {
    if (delta > 0) prev();
    else next();
  }
}

// On the carousel:
<div className="carousel" onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>

The 50px threshold prevents accidental swipes from minor finger movement.

Step 4: autoplay with pause

const [isPaused, setIsPaused] = useState(false);

useEffect(() => {
  if (isPaused) return;
  const id = setInterval(next, 5000);
  return () => clearInterval(id);
}, [isPaused]);

// Pause handlers
<div
  className="carousel"
  onMouseEnter={() => setIsPaused(true)}
  onMouseLeave={() => setIsPaused(false)}
  onFocus={() => setIsPaused(true)}
  onBlur={() => setIsPaused(false)}
>

Step 5: reduced motion support

const prefersReducedMotion = useMemo(() => {
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}, []);

// In useEffect, disable autoplay if reduced motion is preferred:
useEffect(() => {
  if (isPaused || prefersReducedMotion) return;
  const id = setInterval(next, 5000);
  return () => clearInterval(id);
}, [isPaused, prefersReducedMotion]);

Step 6: ARIA live region for slide changes

<div className="visually-hidden" aria-live="polite">
  Slide {current + 1} of {images.length}
</div>

Screen readers announce the current slide as it changes. The visually-hidden class hides it from sight but keeps it accessible.

Common pitfalls

  • No keyboard support. Arrow keys should advance the carousel.
  • No autoplay pause. Aggressive autoplay without pause is hostile to reading.
  • No reduced-motion check. Users with vestibular disorders should not have animations forced on them.
  • No aria-roledescription. Screen readers should know it’s a carousel.
  • All images load eagerly. A 20-image carousel forcing all 20 to load at once hurts LCP.
  • Buttons without aria-label. “←” alone tells screen readers nothing.
  • No wrap-around or wrap-around-only. Some prompts want clamping (no wrap); read the requirements.
  • No animation. Abrupt swaps feel cheap.

Stretch goals

  • Lazy-load images further than just the next/prev (e.g., next 2).
  • Pre-cache the next image in JS to avoid first-show delay.
  • Multi-item display (show 3 slides at once on desktop).
  • Vertical carousel variant.
  • Image zoom on click.
  • Fullscreen / lightbox mode.

Carousels have a long history of accessibility failures. The W3C ARIA Authoring Practices Guide has a specific carousel pattern document. Senior candidates aware of:

  • Carousels are notoriously hostile to screen readers if implemented naively.
  • “role=region” with “aria-roledescription=carousel” is the right framing.
  • Each slide should have an accessible label.
  • Tab order should make sense.
  • Auto-advance should pause when any element inside has keyboard focus.

Time budget for a 45-minute round

  • 0-5 min: clarify requirements (autoplay? touch? wrap-around?).
  • 5-15 min: basic structure with prev/next.
  • 15-25 min: animation, indicators, keyboard support.
  • 25-35 min: touch/swipe, autoplay with pause.
  • 35-40 min: accessibility (ARIA, reduced motion).
  • 40-45 min: edge cases, stretch goals.

Frequently Asked Questions

Should I use a library?

For the round, no. In production, libraries like Embla Carousel and Swiper are excellent choices.

How important is touch support?

For senior+ rounds, expected. The carousel needs to work on mobile.

Is reduced-motion always required?

For senior+ a11y-conscious companies yes. The pattern is small and worth knowing.

How does this differ from infinite scroll?

Carousel: discrete navigation through a fixed set. Infinite scroll: continuous loading of items.

Are carousels still common in 2026?

Less than a decade ago. Many design teams have moved away from auto-rotating carousels because they distract users. The interview round still tests them as a canonical complex-component exercise.

Scroll to Top