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
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.
2. Creating the Basic Carousel Components
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">"{text}"</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>
);
}
7. Styling the Carousel
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:
We set up a React project and installed Embla Carousel.
We created a basic carousel component and added navigation buttons.
We implemented autoplay functionality with pause-on-hover.
We built a custom testimonial card component.
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 inEmblaCarousel.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