Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
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 scrollsOr autoplay doesn’t work:
import Autoplay from 'embla-carousel-autoplay';
const [emblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]);
// Carousel stays on the first slideOr navigation buttons don’t move to the next slide:
Prev/Next buttons render but clicking them does nothingWhy 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 andoverflow: hiddenon 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
divbetween the viewport and slides breaks the scroll calculation. - Autoplay requires the plugin package —
embla-carousel-autoplayis a separate npm package. Importing it without installing causes a module error. The plugin must also be passed as the second argument touseEmblaCarousel. - Navigation needs the Embla API — prev/next buttons must call
emblaApi.scrollPrev()andemblaApi.scrollNext(). The API is available from the second return value ofuseEmblaCarousel.
Fix 1: Basic Carousel with Correct CSS
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>
);
}Fix 6: Vertical Carousel
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 nothing — emblaApi 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lottie Not Working — Animation Not Playing, File Not Loading, or React Component Blank
How to fix Lottie animation issues — lottie-react and lottie-web setup, JSON animation loading, playback control, interactivity, lazy loading, and performance optimization.