AniUI Academy

Build a carousel

mediumUI Components+210 XP

Build a 4-slide carousel that automatically advances every second.

Prev/Next buttons and dot indicators should navigate immediately, independent of the auto-advance timer. Hovering the carousel must pause the auto-advance; moving the mouse away resumes it.

Examples

// Initial render: active-index = 0 ("Slide A")
// After 1000ms → active-index = 1
// After another 1000ms → 2 ... → 3 ... → wraps to 0.

// Click <button data-testid="next" />
// → index moves immediately, independent of the timer.

// Hover the carousel container
// → auto-advance pauses; mouse out resumes it.

Timer runs on an interval; user actions preempt it without resetting the clock.

const pausedRef = useRef(false);

useEffect(() => {
  const id = setInterval(() => {
    if (!pausedRef.current) {
      setIndex((i) => (i + 1) % SLIDES.length);
    }
  }, 1000);
  return () => clearInterval(id);
}, []);

A ref flag toggled in `onMouseEnter`/`onMouseLeave` avoids re-subscribing the interval on every hover.

What it has to do

  • A `data-testid="carousel"` container wraps the whole thing.
  • A `data-testid="active-index"` element shows the current slide index (0-based) as plain text.
  • The active slide advances by one automatically every 1000ms, wrapping from the last slide back to the first.
  • `data-testid="prev"` / `data-testid="next"` move one slide immediately; `data-testid="dot-N"` jumps directly to slide N.
  • While the mouse is over the carousel, automatic advancing is paused; it resumes once the mouse leaves.

Notes

  • Wrap with `(index + length) % length` so prev at 0 goes to the last slide and next at last goes to 0 — one expression for both directions.
  • Autoplay fights accessibility: WCAG asks for a user-visible pause control and for autoplay to stop when the carousel is focused, not just hovered.
  • Clean up the interval in `useEffect`'s return — without it, HMR and unmounts leak timers that keep setting state on a dead tree.
  • Prefer a ref for the pause flag rather than state — toggling state on every mouse enter/leave rebuilds the effect and resets the timer, which users feel as a stutter.
Asked atNetflixAirbnbAmazon

Stuck? The react course covers the ideas this problem is built on.

import { useEffect, useState } from "react";

const SLIDES = ["Slide A", "Slide B", "Slide C", "Slide D"];

export default function App() {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    // TODO: this interval should pause while the mouse is hovering the
    // carousel (see the container below), and resume once it leaves.
    const id = setInterval(() => {
      setIndex((i) => (i + 1) % SLIDES.length);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div
      data-testid="carousel"
      style={{ fontFamily: "sans-serif", padding: 16, maxWidth: 260 }}
    >
      <p>
        Slide: <span data-testid="active-index">{index}</span> — {SLIDES[index]}
      </p>
      <div style={{ display: "flex", gap: 8 }}>
        <button data-testid="prev" onClick={() => setIndex((i) => (i - 1 + SLIDES.length) % SLIDES.length)}>
          Prev
        </button>
        <button data-testid="next" onClick={() => setIndex((i) => (i + 1) % SLIDES.length)}>
          Next
        </button>
      </div>
      <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
        {SLIDES.map((_, i) => (
          <button
            key={i}
            data-testid={`dot-${i}`}
            onClick={() => setIndex(i)}
            style={{ fontWeight: i === index ? "bold" : "normal" }}
          >
            {"\u25CF"}
          </button>
        ))}
      </div>
    </div>
  );
}

5 tests · +210 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.