Embla Carousel Setup
Used in this guide:Introduction
Embla Carousel is a lightweight, customizable slider library that works seamlessly with React and Next.js. Whether you're building a product showcase, image gallery, or interactive section, Embla provides full control with a clean, low-level API. In this tutorial, we'll walk through a step-by-step setup of Embla Carousel in a Next.js 15 project — starting with a basic setup and gradually adding features like separated slide components, arrow controls, and dot navigation.
Install Embla Carousel Library
npm install embla-carousel-reactSetup 1: Basic
This is the most minimal Embla Carousel setup. We use useEmblaCarousel() to create a scrollable container. The emblaRef is attached to the wrapper element, and basic slides are placed inside. This is a great starting point to confirm that Embla is working correctly in your environment.
'use client'
import useEmblaCarousel from "embla-carousel-react";
const Carousel = () => {
const [emblaRef] = useEmblaCarousel({ loop: true });
return (
<div>
<h2 className="mb-4">Basic Setup</h2>
{/* Embla Scope */}
<div ref={emblaRef} className="embla">
<div className="embla__container h-[450px]">
<div className="embla__slide bg-cyan-700 flex justify-center items-center">Slide 1</div>
<div className="embla__slide bg-cyan-800 flex justify-center items-center">Slide 2</div>
<div className="embla__slide bg-cyan-900 flex justify-center items-center">Slide 3</div>
</div>
</div>
{/* --------------------------------- */}
</div>
)
}
export default Carousel;Setup 2: Separated Slide Component
To make the code cleaner and more maintainable, we move each slide into its own reusable <Slide /> component. We also prepare slide data as an array of objects (slideInfo) so it's easier to map over and extend. This is a common pattern in real-world carousels that display content like cities, products, or blog posts.
'use client'
import useEmblaCarousel from "embla-carousel-react";
import Slide from "./Slide";
const slideInfo = [
{
city: "tokyo",
image: "/images/tokyo.jpg",
photographer: "Willian Justen de Vasconcellos"
},
{
city: "barcelona",
image: "/images/barcelona.jpg",
photographer: "Alina Shazka",
},
{
city: "cairo",
image: "/images/cairo.jpg",
photographer: "Alex Azabache",
}
]
const CarouselB = () => {
const [emblaRef] = useEmblaCarousel({ loop: true });
return (
<div>
<h2 className="mb-4">Separated Slide Component Setup</h2>
{/* Embla Scope */}
<div ref={emblaRef} className="embla my-0">
<div className="embla__container h-[450px]">
{slideInfo.map((item) => (
<div key={item.city} className="embla__slide">
<Slide city={item.city} image={item.image} photographer={item.photographer} />
</div>
))}
</div>
</div>
{/* --------------------------------- */}
</div>
)
}
export default CarouselB;import Image from "next/image";
interface SlideProps {
city: string;
image: string;
photographer: string;
}
const Slide = ({ city, image, photographer }: SlideProps ) => {
return (
<div className="flex w-full h-full bg-slate-900">
<div className="flex flex-col gap-2 w-[50%] justify-center items-center">
<h2 className="font-bold text-4xl uppercase text-white">{city}</h2>
<span>By {photographer}</span>
</div>
<div className="flex w-[50%] relative">
<Image
src={image}
alt={city}
fill
priority
className="object-cover overflow-hidden"
/>
</div>
</div>
)
}
export default Slide;Setup 3: Adding Arrow Navigation
In this step, we access the emblaApi returned by useEmblaCarousel() to enable previous/next navigation. Two buttons (< and >) call scrollPrev() and scrollNext() to move between slides programmatically. This gives users more control and improves UX on non-touch devices. Note: We can use the same Slide component made in Setup 2
'use client'
import useEmblaCarousel from "embla-carousel-react";
import Slide from "./Slide";
const slideInfo = [
{
city: "tokyo",
image: "/images/tokyo.jpg",
photographer: "Willian Justen de Vasconcellos"
},
{
city: "barcelona",
image: "/images/barcelona.jpg",
photographer: "Alina Shazka",
},
{
city: "cairo",
image: "/images/cairo.jpg",
photographer: "Alex Azabache",
}
]
const CarouselC = () => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const scrollPrev = () => emblaApi?.scrollPrev();
const scrollNext = () => emblaApi?.scrollNext();
return (
<div>
<div className="flex w-full justify-between items-center">
<h2 className="mb-4">Adding Arrow Navigation</h2>
<div className="flex items-center gap-4 text-xl">
<button onClick={scrollPrev}><</button>
<button onClick={scrollNext}>></button>
</div>
</div>
{/* Embla Scope */}
<div ref={emblaRef} className="embla my-0">
<div className="embla__container h-[450px]">
{slideInfo.map((item) => (
<div key={item.city} className="embla__slide">
<Slide city={item.city} image={item.image} photographer={item.photographer} />
</div>
))}
</div>
</div>
{/* --------------------------------- */}
</div>
)
}
export default CarouselC;Adding Dot Navigation
Here we add dot-based pagination. Using emblaApi.selectedScrollSnap(), we track the currently visible slide and update selectedIndex in React state. Each dot is a button that calls emblaApi.scrollTo(index) to jump directly to a slide. This setup gives your users both swipe and click-based navigation — ideal for carousels with visual indicators. Note: We are using the same Slide component created during CarouselB Setup
'use client'
import useEmblaCarousel from "embla-carousel-react";
import Slide from "./Slide";
import { useEffect, useState } from "react";
const slideInfo = [
{
city: "tokyo",
image: "/images/tokyo.jpg",
photographer: "Willian Justen de Vasconcellos"
},
{
city: "barcelona",
image: "/images/barcelona.jpg",
photographer: "Alina Shazka",
},
{
city: "cairo",
image: "/images/cairo.jpg",
photographer: "Alex Azabache",
}
]
const CarouselD = () => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const scrollPrev = () => emblaApi?.scrollPrev();
const scrollNext = () => emblaApi?.scrollNext();
const scrollTo = (index: number) => emblaApi?.scrollTo(index);
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
if (!emblaApi) return;
const onSelect = () => {
setSelectedIndex(emblaApi.selectedScrollSnap());
};
emblaApi.on('select', onSelect); // listen to slide changes
onSelect(); // run it once on mount
}, [emblaApi])
return (
<div>
<div className="flex w-full justify-between items-center">
<h2 className="mb-4">Adding Dot Navigation</h2>
<div className="flex items-center gap-4 text-xl">
<button onClick={scrollPrev}><</button>
{slideInfo.map((_, i) => (
<button
key={i}
onClick={() => scrollTo(i)}
className={`w-3 h-3 ${
i === selectedIndex ? 'bg-teal-300' : 'bg-gray-700'
}`}
/>
))}
<button onClick={scrollNext}>></button>
</div>
</div>
{/* Embla Scope */}
<div ref={emblaRef} className="embla my-0">
<div className="embla__container h-[450px]">
{slideInfo.map((item) => (
<div key={item.city} className="embla__slide">
<Slide city={item.city} image={item.image} photographer={item.photographer} />
</div>
))}
</div>
</div>
{/* --------------------------------- */}
</div>
)
}
export default CarouselD;Preview
To preview all carousel setups side by side, we include each variation inside the home page. This makes it easier to compare the design and behavior of each version as you scroll.
import Carousel from "./components/Carousel";
import CarouselB from "./components/CarouselB";
import CarouselC from "./components/CarouselC";
import CarouselD from "./components/CarouselD";
export default function Home() {
return (
<div className="flex flex-col gap-[40px] w-full max-w-[1280px] mx-auto py-[40px]">
<Carousel />
<CarouselB />
<CarouselC />
<CarouselD />
</div>
);
}