Skip to content

Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing

FixDevs ·

Quick Answer

How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.

The Problem

The carousel renders but slides don’t scroll:

import useEmblaCarousel from 'embla-carousel-react';

function Carousel() {
  const [emblaRef] = useEmblaCarousel();

  return (
    <div ref={emblaRef}>
      <div>
        <div>Slide 1</div>
        <div>Slide 2</div>
        <div>Slide 3</div>
      </div>
    </div>
  );
}
// All slides are visible and nothing scrolls

Or autoplay doesn’t work:

import Autoplay from 'embla-carousel-autoplay';

const [emblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]);
// Carousel stays on the first slide

Or navigation buttons don’t move to the next slide:

Prev/Next buttons render but clicking them does nothing

Why This Happens

Embla Carousel is a lightweight, dependency-free carousel library. It relies on CSS to define slide sizing and overflow behavior:

  • Slide sizing is controlled by CSS, not JavaScript — Embla doesn’t set slide widths. Without proper CSS (flex: 0 0 100% on slides and overflow: hidden on the viewport), all slides are visible simultaneously and there’s nothing to scroll.
  • The DOM structure must be exact — Embla requires a viewport (ref target) → container (flex wrapper) → slides (flex items) hierarchy. Missing the container div between the viewport and slides breaks the scroll calculation.
  • Autoplay requires the plugin packageembla-carousel-autoplay is a separate npm package. Importing it without installing causes a module error. The plugin must also be passed as the second argument to useEmblaCarousel.
  • Navigation needs the Embla API — prev/next buttons must call emblaApi.scrollPrev() and emblaApi.scrollNext(). The API is available from the second return value of useEmblaCarousel.
npm install embla-carousel-react
'use client';

import useEmblaCarousel from 'embla-carousel-react';
import { useCallback } from 'react';

function Carousel({ slides }: { slides: { id: string; content: React.ReactNode }[] }) {
  const [emblaRef, emblaApi] = useEmblaCarousel({
    align: 'start',
    // loop: true,  // Enable infinite scroll
  });

  const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
  const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);

  return (
    <div className="relative">
      {/* Viewport — overflow hidden */}
      <div className="overflow-hidden" ref={emblaRef}>
        {/* Container — flex row */}
        <div className="flex">
          {/* Slides — flex-shrink: 0, set width */}
          {slides.map(slide => (
            <div
              key={slide.id}
              className="flex-[0_0_100%] min-w-0 px-2"
              // flex-[0_0_100%] = flex: 0 0 100% → one slide per view
              // For 3 slides per view: flex-[0_0_33.333%]
              // For 2 slides per view: flex-[0_0_50%]
            >
              {slide.content}
            </div>
          ))}
        </div>
      </div>

      {/* Navigation buttons */}
      <button
        onClick={scrollPrev}
        className="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white shadow-md flex items-center justify-center hover:bg-gray-50"
        aria-label="Previous slide"
      >

      </button>
      <button
        onClick={scrollNext}
        className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white shadow-md flex items-center justify-center hover:bg-gray-50"
        aria-label="Next slide"
      >

      </button>
    </div>
  );
}

// Usage
<Carousel slides={[
  { id: '1', content: <img src="/image1.jpg" className="w-full rounded-lg" /> },
  { id: '2', content: <img src="/image2.jpg" className="w-full rounded-lg" /> },
  { id: '3', content: <img src="/image3.jpg" className="w-full rounded-lg" /> },
]} />

Fix 2: Dot Indicators

'use client';

import useEmblaCarousel from 'embla-carousel-react';
import { useCallback, useEffect, useState } from 'react';

function CarouselWithDots({ children }: { children: React.ReactNode[] }) {
  const [emblaRef, emblaApi] = useEmblaCarousel();
  const [selectedIndex, setSelectedIndex] = useState(0);
  const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);

  const onSelect = useCallback(() => {
    if (!emblaApi) return;
    setSelectedIndex(emblaApi.selectedScrollSnap());
  }, [emblaApi]);

  useEffect(() => {
    if (!emblaApi) return;
    setScrollSnaps(emblaApi.scrollSnapList());
    emblaApi.on('select', onSelect);
    onSelect();
  }, [emblaApi, onSelect]);

  return (
    <div>
      <div className="overflow-hidden" ref={emblaRef}>
        <div className="flex">
          {children.map((child, i) => (
            <div key={i} className="flex-[0_0_100%] min-w-0">
              {child}
            </div>
          ))}
        </div>
      </div>

      {/* Dot indicators */}
      <div className="flex justify-center gap-2 mt-4">
        {scrollSnaps.map((_, index) => (
          <button
            key={index}
            onClick={() => emblaApi?.scrollTo(index)}
            className={`w-3 h-3 rounded-full transition-colors ${
              index === selectedIndex ? 'bg-blue-500' : 'bg-gray-300'
            }`}
            aria-label={`Go to slide ${index + 1}`}
          />
        ))}
      </div>
    </div>
  );
}

Fix 3: Autoplay Plugin

npm install embla-carousel-autoplay
'use client';

import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
import { useCallback } from 'react';

function AutoplayCarousel({ slides }: { slides: React.ReactNode[] }) {
  const [emblaRef, emblaApi] = useEmblaCarousel(
    { loop: true },
    [
      Autoplay({
        delay: 4000,           // 4 seconds between slides
        stopOnInteraction: true,  // Stop when user interacts
        stopOnMouseEnter: true,   // Pause on hover
        playOnInit: true,          // Start automatically
      }),
    ],
  );

  // Manually control autoplay
  const toggleAutoplay = useCallback(() => {
    const autoplay = emblaApi?.plugins().autoplay;
    if (!autoplay) return;

    if (autoplay.isPlaying()) {
      autoplay.stop();
    } else {
      autoplay.play();
    }
  }, [emblaApi]);

  return (
    <div>
      <div className="overflow-hidden" ref={emblaRef}>
        <div className="flex">
          {slides.map((slide, i) => (
            <div key={i} className="flex-[0_0_100%] min-w-0">
              {slide}
            </div>
          ))}
        </div>
      </div>
      <button onClick={toggleAutoplay}>Toggle Autoplay</button>
    </div>
  );
}

Fix 4: Responsive Slide Sizes

// Different number of slides per view at different breakpoints
// Use CSS for this — Embla reads slide sizes from CSS

function ResponsiveCarousel({ items }: { items: Item[] }) {
  const [emblaRef] = useEmblaCarousel({ align: 'start' });

  return (
    <div className="overflow-hidden" ref={emblaRef}>
      <div className="flex">
        {items.map(item => (
          <div
            key={item.id}
            className="
              flex-[0_0_100%]
              sm:flex-[0_0_50%]
              lg:flex-[0_0_33.333%]
              min-w-0 px-2
            "
            // 1 slide on mobile, 2 on tablet, 3 on desktop
          >
            <div className="bg-white rounded-lg shadow p-4">
              <h3>{item.title}</h3>
              <p>{item.description}</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// With gap between slides
function GappedCarousel() {
  const [emblaRef] = useEmblaCarousel({ align: 'start' });

  return (
    <div className="overflow-hidden" ref={emblaRef}>
      {/* Negative margin to compensate for padding */}
      <div className="flex -ml-4">
        {items.map(item => (
          <div key={item.id} className="flex-[0_0_33.333%] min-w-0 pl-4">
            {/* pl-4 creates the gap */}
            <div className="bg-gray-100 rounded-lg p-4">
              {item.content}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Fix 5: Thumbnail Navigation

'use client';

import useEmblaCarousel from 'embla-carousel-react';
import { useCallback, useEffect, useState } from 'react';

function ThumbnailCarousel({ images }: { images: string[] }) {
  // Main carousel
  const [mainRef, mainApi] = useEmblaCarousel();
  // Thumbnail carousel
  const [thumbRef, thumbApi] = useEmblaCarousel({
    containScroll: 'keepSnaps',
    dragFree: true,
  });

  const [selectedIndex, setSelectedIndex] = useState(0);

  // Sync main → thumbs
  const onSelect = useCallback(() => {
    if (!mainApi || !thumbApi) return;
    const index = mainApi.selectedScrollSnap();
    setSelectedIndex(index);
    thumbApi.scrollTo(index);
  }, [mainApi, thumbApi]);

  useEffect(() => {
    if (!mainApi) return;
    mainApi.on('select', onSelect);
    onSelect();
  }, [mainApi, onSelect]);

  // Click thumb → scroll main
  const onThumbClick = useCallback(
    (index: number) => {
      mainApi?.scrollTo(index);
    },
    [mainApi],
  );

  return (
    <div>
      {/* Main carousel */}
      <div className="overflow-hidden mb-2" ref={mainRef}>
        <div className="flex">
          {images.map((src, i) => (
            <div key={i} className="flex-[0_0_100%] min-w-0">
              <img src={src} className="w-full h-96 object-cover rounded-lg" />
            </div>
          ))}
        </div>
      </div>

      {/* Thumbnail carousel */}
      <div className="overflow-hidden" ref={thumbRef}>
        <div className="flex gap-2">
          {images.map((src, i) => (
            <button
              key={i}
              onClick={() => onThumbClick(i)}
              className={`flex-[0_0_80px] min-w-0 rounded-md overflow-hidden border-2 transition-colors ${
                i === selectedIndex ? 'border-blue-500' : 'border-transparent'
              }`}
            >
              <img src={src} className="w-full h-16 object-cover" />
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}
function VerticalCarousel({ items }: { items: React.ReactNode[] }) {
  const [emblaRef, emblaApi] = useEmblaCarousel({
    axis: 'y',  // Vertical scrolling
    align: 'start',
  });

  return (
    <div
      className="overflow-hidden h-[400px]"  // Fixed height required for vertical
      ref={emblaRef}
    >
      <div className="flex flex-col h-full">
        {items.map((item, i) => (
          <div
            key={i}
            className="flex-[0_0_100%] min-h-0"  // min-h-0 instead of min-w-0
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}

Still Not Working?

All slides visible, nothing scrolls — the CSS is wrong. The viewport needs overflow: hidden. The container needs display: flex. Each slide needs flex: 0 0 <width> (e.g., flex: 0 0 100% for one slide per view). Also add min-width: 0 on slides to prevent flexbox overflow.

Autoplay doesn’t start — verify embla-carousel-autoplay is installed (separate package). The plugin must be passed in an array as the second argument: useEmblaCarousel({ loop: true }, [Autoplay()]). Without loop: true, autoplay stops at the last slide.

Navigation buttons do nothingemblaApi is undefined on first render. Use useCallback with emblaApi in the dependency array. Also check that emblaApi is the second return value: const [emblaRef, emblaApi] = useEmblaCarousel().

Carousel jumps or has wrong spacing — check for CSS that adds unexpected width or padding to slides. Embla calculates positions from the actual DOM dimensions. If CSS changes after mount (e.g., font loading, images loading without dimensions), call emblaApi.reInit() to recalculate.

For related UI component issues, see Fix: Framer Motion Not Working and Fix: dnd-kit Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles