Building an Auto-Scrolling Testimonial Carousel with Embla Carousel in React

In this tutorial, we'll walk through the process of creating an auto-scrolling testimonial carousel using Embla Carousel in a React application. We'll cover everything from setting up the basic structure to implementing advanced features like autoplay and touch controls.

Table of Contents

  1. Setting Up the Project

  2. Creating the Basic Carousel Components

  3. Implementing Navigation Buttons

  4. Adding Autoplay Functionality

  5. Creating the Testimonial Card Component

  6. Implementing the Testimonials Component

  7. Styling the Carousel

1. Setting Up the Project

First, let's set up our project and install the necessary dependencies:

bashCopynpx create-react-app embla-carousel-demo --template typescript
cd embla-carousel-demo
npm install embla-carousel-react embla-carousel-auto-scroll

Before we start building our custom carousel, it's important to note that we'll be using some initial files from the Embla Carousel examples. You can find these files at:

https://www.embla-carousel.com/examples/predefined/#autoplay

Click on "Edit Code" and select the React + TypeScript sandbox to view the initial EmblaCarousel.tsx and EmblaCarouselButtons.tsx files. We'll be modifying these files to suit our needs.

Let's start by creating the basic Embla Carousel component. Create a new file called EmblaCarousel.tsx:

tsxCopy// src/components/EmblaCarousel.tsx
import React, { useCallback, useEffect, useState, useRef } from 'react'
import { EmblaOptionsType } from 'embla-carousel'
import useEmblaCarousel from 'embla-carousel-react'
import AutoScroll from 'embla-carousel-auto-scroll'
import { NextButton, PrevButton, usePrevNextButtons } from './EmblaCarouselButtons'
import TestimonialCard from './TestimonialCard'

type PropType = {
  slides: {
    id: number;
    name: string;
    text: string;
  }[]
  options?: EmblaOptionsType
}

const EmblaCarousel: React.FC<PropType> = (props) => {
  const { slides, options } = props
  const [emblaRef, emblaApi] = useEmblaCarousel(options, [
    AutoScroll({ playOnInit: true })
  ])
  const [, setIsPlaying] = useState(true)
  const longPressTimer = useRef<NodeJS.Timeout | null>(null)

  // ... (we'll add more code here in the following steps)

  return (
    <div className="embla">
      <div className="embla__viewport" ref={emblaRef}>
        <div className="embla__container">
          {slides.map((slide) => (
            <TestimonialCard key={slide.id} {...slide} />
          ))}
        </div>
      </div>
    </div>
  )
}

export default EmblaCarousel

3. Implementing Navigation Buttons

Now, let's create the navigation buttons for our carousel. Create a new file called EmblaCarouselButtons.tsx:

tsxCopy// src/components/EmblaCarouselButtons.tsx
import React, { ComponentPropsWithRef, useCallback, useEffect, useState } from 'react'
import { EmblaCarouselType } from 'embla-carousel'

export const usePrevNextButtons = (
  emblaApi: EmblaCarouselType | undefined,
  onButtonClick?: (emblaApi: EmblaCarouselType) => void
): UsePrevNextButtonsType => {
  const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
  const [nextBtnDisabled, setNextBtnDisabled] = useState(true)

  const onPrevButtonClick = useCallback(() => {
    if (!emblaApi) return
    emblaApi.scrollPrev()
    if (onButtonClick) onButtonClick(emblaApi)
  }, [emblaApi, onButtonClick])

  const onNextButtonClick = useCallback(() => {
    if (!emblaApi) return
    emblaApi.scrollNext()
    if (onButtonClick) onButtonClick(emblaApi)
  }, [emblaApi, onButtonClick])

  const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
    setPrevBtnDisabled(!emblaApi.canScrollPrev())
    setNextBtnDisabled(!emblaApi.canScrollNext())
  }, [])

  useEffect(() => {
    if (!emblaApi) return

    onSelect(emblaApi)
    emblaApi.on('reInit', onSelect).on('select', onSelect)
  }, [emblaApi, onSelect])

  return {
    prevBtnDisabled,
    nextBtnDisabled,
    onPrevButtonClick,
    onNextButtonClick
  }
}

// ... (PrevButton and NextButton components)

4. Adding Autoplay Functionality

Let's update our EmblaCarousel.tsx file to include autoplay functionality:

tsxCopy// src/components/EmblaCarousel.tsx
// ... (previous imports and type definitions)

const EmblaCarousel: React.FC<PropType> = (props) => {
  // ... (previous code)

  usePrevNextButtons(emblaApi)

  const stopAutoplay = useCallback(() => {
    const autoScroll = emblaApi?.plugins()?.autoScroll
    if (autoScroll && autoScroll.isPlaying()) {
      autoScroll.stop()
      setIsPlaying(false)
    }
  }, [emblaApi])

  const startAutoplay = useCallback(() => {
    const autoScroll = emblaApi?.plugins()?.autoScroll
    if (autoScroll && !autoScroll.isPlaying()) {
      autoScroll.play()
      setIsPlaying(true)
    }
  }, [emblaApi])

  const onMouseEnter = useCallback(() => {
    stopAutoplay()
  }, [stopAutoplay])

  const onMouseLeave = useCallback(() => {
    startAutoplay()
  }, [startAutoplay])

  const onTouchStart = useCallback(() => {
    longPressTimer.current = setTimeout(() => {
      stopAutoplay()
    }, 200)
  }, [stopAutoplay])

  const onTouchEnd = useCallback(() => {
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current)
    }
    startAutoplay()
  }, [startAutoplay])

  useEffect(() => {
    const autoScroll = emblaApi?.plugins()?.autoScroll
    if (!autoScroll) return

    setIsPlaying(autoScroll.isPlaying())
    emblaApi
      .on('autoScroll:play', () => setIsPlaying(true))
      .on('autoScroll:stop', () => setIsPlaying(false))
      .on('reInit', () => setIsPlaying(autoScroll.isPlaying()))

    return () => {
      if (longPressTimer.current) {
        clearTimeout(longPressTimer.current)
      }
    }
  }, [emblaApi])

  return (
    <div className="embla">
      <div
        className="embla__viewport"
        ref={emblaRef}
        onMouseEnter={onMouseEnter}
        onMouseLeave={onMouseLeave}
        onTouchStart={onTouchStart}
        onTouchEnd={onTouchEnd}
      >
        {/* ... (previous return statement content) */}
      </div>
    </div>
  )
}

export default EmblaCarousel

5. Creating the Testimonial Card Component

Now, let's create a separate component for our testimonial cards. Create a new file called TestimonialCard.tsx:

tsxCopy// src/components/TestimonialCard.tsx
import React from 'react';

interface SlideProps {
  id: number;
  name: string;
  text: string;
}

const TestimonialCard: React.FC<SlideProps> = ({ id, name, text }) => {
  return (
    <div className="embla__slide">
      <div className="">
        <div key={id} className="bg-white shadow-lg rounded-lg p-6">
          <p className="text-gray-600 mb-4">&quot;{text}&quot;</p>
          <p className="font-semibold text-gray-900">- {name}</p>
        </div>
      </div>
    </div>
  );
};

export default TestimonialCard;

6. Implementing the Testimonials Component

Finally, let's create a Testimonials component that will use our EmblaCarousel. Create a new file called Testimonials.tsx:

tsxCopy// src/components/Testimonials.tsx
import React from 'react';
import EmblaCarousel from "./EmblaCarousel";
import { EmblaOptionsType } from 'embla-carousel'

const testimonials = [
  { id: 1, name: "John Doe", text: "PropertyLink helped me find my dream home. The process was smooth and easy!" },
  { id: 2, name: "Jane Smith", text: "I sold my property quickly thanks to PropertyLink. Their team is professional and efficient." },
  { id: 3, name: "Mike Johnson", text: "Great selection of properties and excellent customer service. Highly recommended!" },
  { id: 4, name: "Sarah Brown", text: "The PropertyLink team went above and beyond to help me find the perfect rental property." },
  { id: 5, name: "David Lee", text: "I've used PropertyLink for both buying and selling. They're simply the best in the business!" },
];

const OPTIONS: EmblaOptionsType = { loop: true }

export default function Testimonials() {
  return (
    <div className="bg-gray-100 py-12">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <h2 className="text-3xl font-extrabold text-gray-900 text-center mb-8">What Our Clients Say</h2>
        <div className="grid grid-cols-1 gap-8">
          <EmblaCarousel slides={testimonials} options={OPTIONS} />
        </div>
      </div>
    </div>
  );
}

To style the carousel, we'll use a dedicated CSS file. Just like the initial TypeScript files, you can obtain the CSS file from the Embla Carousel examples page

Create a new file called embla.css in your src/styles/ directory and copy the contents from the example. The CSS file should look like this:

cssCopy/* src/styles/embla.css */
.embla {
  max-width: 100%;
  margin: auto;
  --slide-height: 19rem;
  --slide-spacing: 1rem;
  --slide-size: 45%;
}
.embla__viewport {
  overflow: hidden;
}
.embla__container {
  display: flex;
  touch-action: pan-y pinch-zoom;
  margin-left: calc(var(--slide-spacing) * -1);
}
.embla__slide {
  flex: 0 0 var(--slide-size);
  min-width: 0;
  padding-left: var(--slide-spacing);
  position: relative;
}
.embla__slide__number {
  width: 4.6rem;
  height: 4.6rem;
  z-index: 1;
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  border-radius: 50%;
  background-color: rgba(var(--background-site-rgb-value), 0.85);
  line-height: 4.6rem;
  font-weight: 900;
  text-align: center;
  pointer-events: none;
}
.embla__slide__number > span {
  color: var(--brand-primary);
  background-image: linear-gradient(
    45deg,
    var(--brand-primary),
    var(--brand-secondary)
  );
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-size: 1.6rem;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.embla__button {
  -webkit-appearance: none;
  background-color: transparent;
  touch-action: manipulation;
  display: inline-flex;
  text-decoration: none;
  cursor: pointer;
  border: 0;
  padding: 0;
  margin: 0;
}
.embla__buttons {
  display: flex;
  align-items: center;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  left: 1.6rem;
}
.embla__button {
  z-index: 1;
  color: var(--background-site);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  width: 4rem;
  height: 4rem;
}
.embla__button:disabled {
  opacity: 0.3;
}
.embla__button__svg {
  width: 65%;
  height: 65%;
}
.embla__dot {
  -webkit-appearance: none;
  background-color: transparent;
  touch-action: manipulation;
  display: inline-flex;
  text-decoration: none;
  cursor: pointer;
  border: 0;
  padding: 0;
  margin: 0;
}
.embla__dots {
  z-index: 1;
  bottom: 1.6rem;
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}
.embla__dot {
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  margin-right: 0.75rem;
  margin-left: 0.75rem;
}
.embla__dot:after {
  background: var(--background-site);
  border-radius: 0.2rem;
  width: 100%;
  height: 0.3rem;
  content: '';
}
.embla__dot--selected:after {
  background: linear-gradient(
    45deg,
    var(--brand-primary),
    var(--brand-secondary)
  );
}

import this CSS file in your main App.tsx or index.tsx file:

tsxCopyimport './styles/embla.css';

By using the CSS provided in the Embla Carousel examples, you ensure that your carousel will have the same professional and polished look as the demo, with responsive sizing, smooth transitions, and styled navigation buttons.

Conclusion

Congratulations! You've just built a slick, auto-scrolling testimonial carousel using Embla Carousel in React. Let's recap what we've accomplished:

  1. We set up a React project and installed Embla Carousel.

  2. We created a basic carousel component and added navigation buttons.

  3. We implemented autoplay functionality with pause-on-hover.

  4. We built a custom testimonial card component.

  5. We styled our carousel to make it look professional and responsive.

The beauty of this setup is its flexibility. While we've used the initial files from Embla Carousel examples as a starting point, don't be afraid to tinker with the code. You can easily customize the carousel to fit your specific needs:

  • Want a different layout? Adjust the CSS to change the slide size, spacing, or overall carousel width.

  • Need more or fewer slides visible at once? Modify the --slide-size CSS variable.

  • Want to change the autoplay speed? Adjust the AutoScroll options in EmblaCarousel.tsx.

  • Prefer a different style for the navigation buttons? Update the SVG paths in EmblaCarouselButtons.tsx.

Remember, the code we've written is just a foundation. As you become more comfortable with Embla Carousel, you'll find countless ways to extend and customize its functionality.

To use your new carousel, just import the Testimonials component wherever you need it in your app:

tsxCopyimport Testimonials from './components/Testimonials';

function App() {
  return (
    <div className="App">
      <header>Welcome to Our Site</header>
      <Testimonials />
      <footer>© 2023 Your Company</footer>
    </div>
  );
}

And there you have it! You've now got a responsive, accessible, and customizable carousel that'll make your testimonials shine. It'll automatically scroll through your glowing reviews, pause when users want to read more closely, and provide a smooth, professional feel to your site.

As you continue to work with this carousel, you might find new features you want to add or tweaks you want to make. That's the joy of web development – there's always room for improvement and innovation. So go ahead, play around with the code, and make this carousel truly your own.

Happy coding!

Created with Claude.ai