ReactHustle

How to Create a Carousel in NextJS using TailwindCSS and EmblaCarousel

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a Carousel in NextJS using TailwindCSS and EmblaCarousel

Hello, hustlers! In this tutorial, you'll learn how to create a fully customizable carousel component in NextJS or React using TailwindCSS and the embla-carousel-react package.

What is EmblaCarousel? #

EmblaCarousel is a lightweight (6.3kb min+gzipped), 100% open source, library agnostic carousel library with fluid motion and supports swipe gestures. It also does not have any dependencies on other libraries.

The embla-carousel-react package is a simple React wrapper to the embla-carousel library, so that we can easily access the states, events, and functions that it provides.

By combining it with React and TailwindCSS, it's very easy (and fast) to create your own carousel implementation.

Final Output #

This is the carousel that we'll be making in this tutorial:

NextJS Carousel Tutorial - Final Output

Tutorial Objectives #

  1. Use NextJS as our react framework
  2. Install TailwindCSS to NextJS. How to Set Up NextJS and TailwindCSS in 2023
  3. Create a simple carousel component
  4. Add other components like <Dots/> , prev button, and next button components.

By the end of this tutorial, you'll learn how to create a carousel with any design imaginable. If the information provided in this tutorial is insufficient, you can always refer to the EmblaCarousel documentation

Step 0 - Project Setup #

To speed up the tutorial, let's create a new NextJS project with TailwindCSS pre-installed. If you are working on an existing NextJS project, please see my previous tutorial:  How to Set up Nextjs and TailwindCSS.

Run the following command to bootstrap our project:

npx create-next-app -e https://github.com/jmarioste/next-tailwind-starter-2 next-carousel

Once everything is done, you can run the following commands to start the local dev server:

cd next-carousel
yarn dev

Step 1 - Installing Dependencies #

Let's install embla-carousel-react and classnames packages. The classnames package is always useful if you're using TailwindCSS since it's unavoidable to deal with toggling classes.

Use the following command to install both of these packages:

yarn add embla-carousel-react classnames

Step 2 - Creating a Simple Carousel #

Now that we're done with the setup, we can proceed with the implementation. First, create the file components/Carousel.tsx.

// components/Carousel.tsx
// import the hook and options type
import useEmblaCarousel, { EmblaOptionsType } from "embla-carousel-react";
import { PropsWithChildren } from "react";

// Define the props
type Props = PropsWithChildren & EmblaOptionsType;

const Carousel = ({ children, ...options }: Props) => {
  // 1. useEmblaCarousel returns a emblaRef and we must attach the ref to a container.
  // EmblaCarousel will use that ref as basis for swipe and other functionality.
  const [emblaRef] = useEmblaCarousel(options);

  return (
    // Attach ref to a div
    // 2. The wrapper div must have overflow:hidden
    <div className="overflow-hidden" ref={emblaRef}>
      {/* 3. The inner div must have a display:flex property */}
      {/* 4. We pass the children as-is so that the outside component can style it accordingly */}
      <div className="flex">{children}</div>
    </div>
  );
};
export default Carousel;
123456789101112131415161718192021222324

Usage

To use this component, we just pass the options and style each slide using the flex property. For example:

import type { NextPage } from "next";
import Carousel from "../components/Carousel";
import Image from "next/image";
const Home: NextPage = () => {
  const images = [
    "https://placehold.co/480x300?font=roboto&text=Slide+1",
    "https://placehold.co/480x300?font=roboto&text=Slide+2",
    "https://placehold.co/480x300?font=roboto&text=Slide+3",
    "https://placehold.co/480x300?font=roboto&text=Slide+4",
  ];
  return (
    <div className="lg:w-3/4 mx-auto my-2">
      <Carousel loop>
        {images.map((src, i) => {
          return (
            // 👇 style each individual slide.
            // relative - needed since we use the fill prop from next/image component
            // h-64 - arbitrary height
            // flex[0_0_100%]
            //   - shorthand for flex-grow:0; flex-shrink:0; flex-basis:100%
            //   - we want this slide to not be able to grow or shrink and take up 100% width of the viewport.
            <div className="relative h-64 flex-[0_0_100%]" key={i}>
              {/* use object-cover + fill since we don't know the height and width of the parent */}
              <Image src={src} fill className="object-cover" alt="alt" />
            </div>
          );
        })}
      </Carousel>
    </div>
  );
};

export default Home;

123456789101112131415161718192021222324252627282930313233

NOTE: Don't forget to add domains in your next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  images: {
    domains: ["placehold.co"],
  },
};

module.exports = nextConfig;
12345678910

The output should be a simple carousel that has swipe functionality like this:

Next JS Carousel Tutorial - Step 2 Output

If you only need a simple carousel like this, you're should be done at this point. In the next steps of this tutorial, we'll improve this carousel by adding the <Dots/> and <CarouselControl/> components.

Step 3 - Implementing the <Dots/> Component #

To implement the Dots component, we'll need access to the emblaApi state. The emblaApi is returned as the 2nd element from the useEmblaCarousel hook. Let's make some changes to our Carousel component to make this happen:

// components/Carousel.tsx
import useEmblaCarousel, { EmblaOptionsType } from "embla-carousel-react";
import { PropsWithChildren, useEffect, useState } from "react";

type Props = PropsWithChildren & EmblaOptionsType;

const Carousel = ({ children, ...options }: Props) => {
  const [emblaRef, emblaApi] = useEmblaCarousel(options);

  // We need to track the selectedIndex to allow this component to re-render in react.
  // Since emblaRef is a ref, it won't re-render even if there are internal changes to its state.
  const [selectedIndex, setSelectedIndex] = useState(0);

  useEffect(() => {
    function selectHandler() {
      // selectedScrollSnap gives us the current selected index.
      const index = emblaApi?.selectedScrollSnap();
      setSelectedIndex(index || 0);
    }

    emblaApi?.on("select", selectHandler);
    // cleanup
    return () => {
      emblaApi?.off("select", selectHandler);
    };
  }, [emblaApi]);

  return (
    <div className="overflow-hidden" ref={emblaRef}>
      <div className="flex">{children}</div>
    </div>
  );
};
export default Carousel;
12345678910111213141516171819202122232425262728293031323334

Now that we have access to the selectedIndex, let's create the file components/Dots.tsx.

import classNames from "classnames";

type Props = {
  itemsLength: number;
  selectedIndex: number;
};
const Dots = ({ itemsLength, selectedIndex }: Props) => {
  const arr = new Array(itemsLength).fill(0);
  return (
    <div className="flex gap-1 my-2 justify-center -translate-y-5">
      {arr.map((_, index) => {
        const selected = index === selectedIndex;
        return (
          <div
            className={classNames({
              "h-2 w-2 rounded-full transition-all duration-300 bg-indigo-400":
                true,
              // tune down the opacity if slide is not selected
              "opacity-50": !selected,
            })}
            key={index}
          ></div>
        );
      })}
    </div>
  );
};
export default Dots;
12345678910111213141516171819202122232425262728

In the above code, we're creating a dot for each slide, and styling the current slide to have a darker color than the rest.

Let's use this inside the Carousel component:

// ...
const Carousel = ({ children, ...options }: Props) => {
  // ...
  const length = React.Children.count(children);
  return (
    <>
      <div className="overflow-hidden" ref={emblaRef}>
        <div className="flex">{children}</div>
      </div>
      <Dots itemsLength={length} selectedIndex={selectedIndex} />
    </>
  );
};
12345678910111213

After this step, you should have the following output:

NextJS Carousel Step 3

Final Step - Adding the <CarouselControls/> Component #

In this step, we will add the next and previous buttons to our component. Fortunately, the emblaApi already provides us with some functions that we can use to simplify the implementation: canScrollPrev(), canScrollNext(), scrollPrev(), and scrollNext().

First, create the file components/CarouselControls.tsx:

// components/CarouselControls.tsx
import classNames from "classnames";

type Props = {
  canScrollPrev: boolean;
  canScrollNext: boolean;
  onPrev(): void;
  onNext(): void;
};
const CarouselControls = (props: Props) => {
  return (
    <div className="flex justify-end gap-2 ">
      <button
        onClick={() => {
          if (props.canScrollPrev) {
            props.onPrev();
          }
        }}
        disabled={!props.canScrollPrev}
        className={classNames({
          "px-4 py-2 text-white rounded-md": true,
          "bg-indigo-200": !props.canScrollPrev,
          "bg-indigo-400": props.canScrollPrev,
        })}
      >
        Prev
      </button>
      <button
        onClick={() => {
          if (props.canScrollNext) {
            props.onNext();
          }
        }}
        disabled={!props.canScrollNext}
        className={classNames({
          "px-4 py-2 text-white rounded-md": true,
          "bg-indigo-200": !props.canScrollNext,
          "bg-indigo-400": props.canScrollNext,
        })}
      >
        Next
      </button>
    </div>
  );
};
export default CarouselControls;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

In the above code, we render two buttons. We attach click handlers for both buttons. We also use the canScrollNext, and canScrollPrev states to style the buttons if it's disabled or not.

Next, let's use this in our <Carousel/> component:

// components/Carousel.tsx
// import the hook and options type
import useEmblaCarousel, { EmblaOptionsType } from "embla-carousel-react";
import React from "react";
import { PropsWithChildren, useEffect, useState } from "react";
import CarouselControls from "./CarouselControls";
import Dots from "./Dots";
// ...

const Carousel = ({ children, ...options }: Props) => {
  // ...
  const length = React.Children.count(children);
  const canScrollNext = !!emblaApi?.canScrollNext();
  const canScrollPrev = !!emblaApi?.canScrollPrev();
  return (
    <>
      <div className="overflow-hidden" ref={emblaRef}>
        <div className="flex">{children}</div>
      </div>
      <Dots itemsLength={length} selectedIndex={selectedIndex} />
      <CarouselControls
        canScrollNext={canScrollNext}
        canScrollPrev={canScrollPrev}
        onNext={() => emblaApi?.scrollNext()}
        onPrev={() => emblaApi?.scrollPrev()}
      />
    </>
  );
};
export default Carousel;
123456789101112131415161718192021222324252627282930

That's it! After this step, you should have the same output as our final output.

Full Code and Demo #

The full code is available at Github: NextJS Carousel Tutorial

The demo is available at StackBlitz: NextJS Carousel Tutorial

Conclusion #

We learned how to implement a react carousel component using EmblaCarousel and we used TailwindCSS to customize it to our liking. EmblaCarousel is an amazing carousel library, so if you haven't tried it before, it's definitely worth checking out.

Resources #

Credits: Image by Harald Tedesco from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle