How to Build a Fancy Testimonial Slider with Tailwind CSS and Next.js
Tags: nextjs, react, tailwindcss, tutorial
Published: 2023-06-20 09:01:20

####**[Live Demo](https://cruip-tutorials-next.vercel.app/fancy-testimonials-slider) / [Download](https://github.com/cruip/cruip-tutorials-next/blob/main/components/fancy-testimonials-slider.tsx)**

--

Welcome to the second part of the series of **How to Build a Fancy Testimonial Slider with Tailwind CSS**! In the previous tutorial, we learned how to create a testimonial slider using Alpine.js. Now, get ready to take your skills to the next level, as we will build a powerful **Next.js component** that achieves the same stunning result.

The significance of customer testimonials in driving your visitors’ purchasing decisions for your product or service is widely acknowledged and understood, so if you want to have a look at how we previously developed this component for our templates, we recommend checking out our [Dark Next.js landing page template](https://cruip.com/stellar/) called **Stellar**.

Let’s get started!

Creating the component
----------------------

But enough talking, let’s start by creating a file `fancy-testimonial-slider.tsx` for our component:

```tsx
  'use client'
  
  import { useState } from 'react'
  import Image, { StaticImageData } from 'next/image'
  
  import TestimonialImg01 from '@/public/testimonial-01.jpg'
  import TestimonialImg02 from '@/public/testimonial-02.jpg'
  import TestimonialImg03 from '@/public/testimonial-03.jpg'
  
  interface Testimonial {
    img: StaticImageData
    quote: string
    name: string
    role: string
  }
  
  export default function FancyTestimonialsSlider() {
  
    const [active, setActive] = useState<number>(0)
    const [autorotate, setAutorotate] = useState<boolean>(true)
    const autorotateTiming: number = 7000
  
    const testimonials: Testimonial[] = [
      {
        img: TestimonialImg01,
        quote: "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.",
        name: 'Jessie J',
        role: 'Acme LTD'
      },
      {
        img: TestimonialImg02,
        quote: "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.",
        name: 'Nick V',
        role: 'Malika Inc.'
      },
      {
        img: TestimonialImg03,
        quote: "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.",
        name: 'Amelia W',
        role: 'Panda AI'
      }
    ]
  
    return (
      <div className="w-full max-w-3xl mx-auto text-center">
        {/* ... */}
      </div>
    )
  }
```

As usual, we have declared `'use client'` at the very top of the file to indicate that the component will be executed on the client-side. Additionally, we have created three state variables that are needed to control the slider:

*   `active` to keep track of the active testimonial
*   `autorotate` to enable or disable automatic rotation
*   `autorotateTiming` to set the time for automatic rotation

Next, we’ve defined an array of testimonials with their respective properties. Leveraging TypeScript, we’ve also introduced the `Testimonial` interface to specify the type of each array element.

Now, inside the component, we can start building the HTML structure:

```tsx
  'use client'
  
  import { useState } from 'react'
  import Image, { StaticImageData } from 'next/image'
  
  import TestimonialImg01 from '@/public/testimonial-01.jpg'
  import TestimonialImg02 from '@/public/testimonial-02.jpg'
  import TestimonialImg03 from '@/public/testimonial-03.jpg'
  
  interface Testimonial {
    img: StaticImageData
    quote: string
    name: string
    role: string
  }
  
  export default function FancyTestimonialsSlider() {
  
    const [active, setActive] = useState<number>(0)
    const [autorotate, setAutorotate] = useState<boolean>(true)
    const autorotateTiming: number = 7000
  
    const testimonials: Testimonial[] = [
      {
        img: TestimonialImg01,
        quote: "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.",
        name: 'Jessie J',
        role: 'Acme LTD'
      },
      {
        img: TestimonialImg02,
        quote: "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.",
        name: 'Nick V',
        role: 'Malika Inc.'
      },
      {
        img: TestimonialImg03,
        quote: "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.",
        name: 'Amelia W',
        role: 'Panda AI'
      }
    ]
  
    return (
      <div className="w-full max-w-3xl mx-auto text-center">
        {/* Testimonial image */}
        <div className="relative h-32">
          <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[480px] h-[480px] pointer-events-none before:absolute before:inset-0 before:bg-gradient-to-b before:from-indigo-500/25 before:via-indigo-500/5 before:via-25% before:to-indigo-500/0 before:to-75% before:rounded-full before:-z-10">
            <div className="h-32 [mask-image:_linear-gradient(0deg,transparent,theme(colors.white)_20%,theme(colors.white))]">
  
              {testimonials.map((testimonial, index) => (
                <Image key={index} className="relative top-11 left-1/2 -translate-x-1/2 rounded-full" src={testimonial.img} width={56} height={56} alt={testimonial.name} />
              ))}
  
            </div>
          </div>
        </div>
        {/* Text */}
        <div className="mb-9 transition-all duration-150 delay-300 ease-in-out">
          <div className="relative flex flex-col">
  
            {testimonials.map((testimonial, index) => (
              <div key={index} className="text-2xl font-bold text-slate-900 before:content-['\201C'] after:content-['\201D']">{testimonial.quote}</div>
            ))}
  
          </div>
        </div>
        {/* Buttons */}
        <div className="flex flex-wrap justify-center -m-1.5">
  
          {testimonials.map((testimonial, index) => (
            <button
              key={index}
              className={`inline-flex justify-center whitespace-nowrap rounded-full px-3 py-1.5 m-1.5 text-xs shadow-sm focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ${active === index ? 'bg-indigo-500 text-white shadow-indigo-950/10' : 'bg-white hover:bg-indigo-100 text-slate-900'}`}
              onClick={() => { setActive(index); }}
            >
              <span>{testimonial.name}</span> <span className={`${active === index ? 'text-indigo-200' : 'text-slate-300'}`}>-</span> <span>{testimonial.role}</span>
            </button>
          ))}
  
        </div>
      </div>
    )
  }
```

As you can see, we have used the `map()` method to iterate over the array of testimonials and dynamically generate the corresponding HTML elements. For each testimonial, we’ve created an `Image` element to display the associated image, a `div` to present the text, and a `button` for the button.

In addition, we implemented the `onClick()` handler to update the active testimonial index when a button is clicked.

However, the component is not yet functional as it renders all the images and text for each testimonial. Instead, we want to display only the active testimonial and introduce smooth transitions when switching between testimonials.

Managing the active testimonial and adding transitions
------------------------------------------------------

As we did with the modal video component created in a previous tutorial, we will use the [Headless UI](https://headlessui.com/) library for managing transitions. So, if we haven’t already, we need to install the library using the command `npm install @headlessui/react@latest`.

Once the library is installed, make sure to import the `Transition` component and wrap both the image and text of our testimonial within it. Also, don’t forget to include the `key={index}` property within the `Transition` component:

```tsx
  'use client'
  
  import { useState } from 'react'
  import Image, { StaticImageData } from 'next/image'
  import { Transition } from '@headlessui/react'
  
  import TestimonialImg01 from '@/public/testimonial-01.jpg'
  import TestimonialImg02 from '@/public/testimonial-02.jpg'
  import TestimonialImg03 from '@/public/testimonial-03.jpg'
  
  interface Testimonial {
    img: StaticImageData
    quote: string
    name: string
    role: string
  }
  
  export default function FancyTestimonialsSlider() {
  
    const [active, setActive] = useState<number>(0)
    const [autorotate, setAutorotate] = useState<boolean>(true)
    const autorotateTiming: number = 7000
  
    const testimonials: Testimonial[] = [
      {
        img: TestimonialImg01,
        quote: "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.",
        name: 'Jessie J',
        role: 'Acme LTD'
      },
      {
        img: TestimonialImg02,
        quote: "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.",
        name: 'Nick V',
        role: 'Malika Inc.'
      },
      {
        img: TestimonialImg03,
        quote: "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.",
        name: 'Amelia W',
        role: 'Panda AI'
      }
    ]
  
    return (
      <div className="w-full max-w-3xl mx-auto text-center">
        {/* Testimonial image */}
        <div className="relative h-32">
          <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[480px] h-[480px] pointer-events-none before:absolute before:inset-0 before:bg-gradient-to-b before:from-indigo-500/25 before:via-indigo-500/5 before:via-25% before:to-indigo-500/0 before:to-75% before:rounded-full before:-z-10">
            <div className="h-32 [mask-image:_linear-gradient(0deg,transparent,theme(colors.white)_20%,theme(colors.white))]">
  
              {testimonials.map((testimonial, index) => (
                <Transition
                  key={index}
                  show={active === index}
                  className="absolute inset-0 h-full -z-10"
                  enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 order-first"
                  enterFrom="opacity-0 -rotate-[60deg]"
                  enterTo="opacity-100 rotate-0"
                  leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700"
                  leaveFrom="opacity-100 rotate-0"
                  leaveTo="opacity-0 rotate-[60deg]"
                >
                  <Image className="relative top-11 left-1/2 -translate-x-1/2 rounded-full" src={testimonial.img} width={56} height={56} alt={testimonial.name} />
                </Transition>
              ))}
  
            </div>
          </div>
        </div>
        {/* Text */}
        <div className="mb-9 transition-all duration-150 delay-300 ease-in-out">
          <div className="relative flex flex-col">
  
            {testimonials.map((testimonial, index) => (
              <Transition
                key={index}
                show={active === index}
                enter="transition ease-in-out duration-500 delay-200 order-first"
                enterFrom="opacity-0 -translate-x-4"
                enterTo="opacity-100 translate-x-0"
                leave="transition ease-out duration-300 delay-300 absolute"
                leaveFrom="opacity-100 translate-x-0"
                leaveTo="opacity-0 translate-x-4"
              >
                <div className="text-2xl font-bold text-slate-900 before:content-['\201C'] after:content-['\201D']">{testimonial.quote}</div>
              </Transition>
            ))}
  
          </div>
        </div>
        {/* Buttons */}
        <div className="flex flex-wrap justify-center -m-1.5">
  
          {testimonials.map((testimonial, index) => (
            <button
              key={index}
              className={`inline-flex justify-center whitespace-nowrap rounded-full px-3 py-1.5 m-1.5 text-xs shadow-sm focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ${active === index ? 'bg-indigo-500 text-white shadow-indigo-950/10' : 'bg-white hover:bg-indigo-100 text-slate-900'}`}
              onClick={() => { setActive(index); }}
            >
              <span>{testimonial.name}</span> <span className={`${active === index ? 'text-indigo-200' : 'text-slate-300'}`}>-</span> <span>{testimonial.role}</span>
            </button>
          ))}
  
        </div>
      </div>
    )
  }
```

Improving transitions between testimonials with varying heights
---------------------------------------------------------------

At this point, to enhance the user experience when transitioning between testimonials with different heights, we will create a method called `heightFix()`. This method calculates the height of the current testimonial and applies it to the container element. By incorporating the classes `transition-all duration-150 delay-300 ease-in-out` to the container element, the height will transition smoothly when switching between testimonials of varying heights.

To accomplish this, we first need to reference the element for which we want to calculate the height using the `ref={testimonialsRef}` attribute. Next, we will define the `heightFix()` method:

```tsx
    const heightFix = () => {
      if (testimonialsRef.current && testimonialsRef.current.parentElement) testimonialsRef.current.parentElement.style.height = `${testimonialsRef.current.clientHeight}px`
    }
```

This method should be triggered in two situations: on component mount (using the `useEffect` hook), and whenever there is a transition between testimonials, utilizing the `beforeEnter` callback provided by the `Transition` component:

```tsx
  'use client'
  
  import { useState, useRef, useEffect } from 'react'
  import Image, { StaticImageData } from 'next/image'
  import { Transition } from '@headlessui/react'
  
  import TestimonialImg01 from '@/public/testimonial-01.jpg'
  import TestimonialImg02 from '@/public/testimonial-02.jpg'
  import TestimonialImg03 from '@/public/testimonial-03.jpg'
  
  interface Testimonial {
    img: StaticImageData
    quote: string
    name: string
    role: string
  }
  
  export default function FancyTestimonialsSlider() {
  
    const testimonialsRef = useRef<HTMLDivElement>(null)
    const [active, setActive] = useState<number>(0)
    const [autorotate, setAutorotate] = useState<boolean>(true)
    const autorotateTiming: number = 7000
  
    const testimonials: Testimonial[] = [
      {
        img: TestimonialImg01,
        quote: "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.",
        name: 'Jessie J',
        role: 'Acme LTD'
      },
      {
        img: TestimonialImg02,
        quote: "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.",
        name: 'Nick V',
        role: 'Malika Inc.'
      },
      {
        img: TestimonialImg03,
        quote: "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.",
        name: 'Amelia W',
        role: 'Panda AI'
      }
    ]
    
    const heightFix = () => {
      if (testimonialsRef.current && testimonialsRef.current.parentElement) testimonialsRef.current.parentElement.style.height = `${testimonialsRef.current.clientHeight}px`
    }
  
    useEffect(() => {
      heightFix()
    }, [])
  
    return (
      <div className="w-full max-w-3xl mx-auto text-center">
        {/* Testimonial image */}
        <div className="relative h-32">
          <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[480px] h-[480px] pointer-events-none before:absolute before:inset-0 before:bg-gradient-to-b before:from-indigo-500/25 before:via-indigo-500/5 before:via-25% before:to-indigo-500/0 before:to-75% before:rounded-full before:-z-10">
            <div className="h-32 [mask-image:_linear-gradient(0deg,transparent,theme(colors.white)_20%,theme(colors.white))]">
  
              {testimonials.map((testimonial, index) => (
                <Transition
                  key={index}
                  show={active === index}
                  className="absolute inset-0 h-full -z-10"
                  enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 order-first"
                  enterFrom="opacity-0 -rotate-[60deg]"
                  enterTo="opacity-100 rotate-0"
                  leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700"
                  leaveFrom="opacity-100 rotate-0"
                  leaveTo="opacity-0 rotate-[60deg]"
                >
                  <Image className="relative top-11 left-1/2 -translate-x-1/2 rounded-full" src={testimonial.img} width={56} height={56} alt={testimonial.name} />
                </Transition>
              ))}
  
            </div>
          </div>
        </div>
        {/* Text */}
        <div className="mb-9 transition-all duration-150 delay-300 ease-in-out">
          <div className="relative flex flex-col" ref={testimonialsRef}>
  
            {testimonials.map((testimonial, index) => (
              <Transition
                key={index}
                show={active === index}
                enter="transition ease-in-out duration-500 delay-200 order-first"
                enterFrom="opacity-0 -translate-x-4"
                enterTo="opacity-100 translate-x-0"
                leave="transition ease-out duration-300 delay-300 absolute"
                leaveFrom="opacity-100 translate-x-0"
                leaveTo="opacity-0 translate-x-4"
                beforeEnter={() => heightFix()}
              >
                <div className="text-2xl font-bold text-slate-900 before:content-['\201C'] after:content-['\201D']">{testimonial.quote}</div>
              </Transition>
            ))}
  
          </div>
        </div>
        {/* Buttons */}
        <div className="flex flex-wrap justify-center -m-1.5">
  
          {testimonials.map((testimonial, index) => (
            <button
              key={index}
              className={`inline-flex justify-center whitespace-nowrap rounded-full px-3 py-1.5 m-1.5 text-xs shadow-sm focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ${active === index ? 'bg-indigo-500 text-white shadow-indigo-950/10' : 'bg-white hover:bg-indigo-100 text-slate-900'}`}
              onClick={() => { setActive(index); }}
            >
              <span>{testimonial.name}</span> <span className={`${active === index ? 'text-indigo-200' : 'text-slate-300'}`}>-</span> <span>{testimonial.role}</span>
            </button>
          ))}
  
        </div>
      </div>
    )
  }
```

Adding the auto-rotate feature
------------------------------

The final step of the tutorial is to add the auto-rotate functionality. Additionally, to ensure an optimal user experience, we want to disable auto-rotation when the user interacts with the buttons.

As you may have already noticed, we defined the `autorotate` and `autorotateTiming` variables from the beginning. The `autorotate` variable determines whether auto-rotation is active or not, while the `autorotateTiming` variable sets the time interval between each slide.

To activate the auto-rotate feature, we can simply add a `useEffect` hook that triggers whenever there are changes in the `active` or `autorotate` state variables:

```tsx
  useEffect(() => {
    if (!autorotate) return
    const interval = setInterval(() => {
      setActive(active + 1 === testimonials.length ? 0 : active => active + 1)
    }, autorotateTiming)
    return () => clearInterval(interval)
  }, [active, autorotate])
```

Finally, to disable auto-rotation when the user interacts with the buttons, we can simply set the `autorotate` state variable to `false` on the `onClick` event:

```tsx
  <button
    key={index}
    className={`inline-flex justify-center whitespace-nowrap rounded-full px-3 py-1.5 m-1.5 text-xs shadow-sm focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ${active === index ? 'bg-indigo-500 text-white shadow-indigo-950/10' : 'bg-white hover:bg-indigo-100 text-slate-900'}`}
    onClick={() => { setActive(index); setAutorotate(false); }}
  >
    <span>{testimonial.name}</span> <span className={`${active === index ? 'text-indigo-200' : 'text-slate-300'}`}>-</span> <span>{testimonial.role}</span>
  </button>
```

The component is now complete. However, it is currently not reusable because we have defined the content of the testimonials within the component itself. To make the component reusable, some modifications are necessary. Let’s see what needs to be done.

Create a reusable testimonials component
----------------------------------------

To make the component reusable, we need to move the testimonials’ content to the parent component (in our case, the `page.tsx` file). Then, we will pass the `testimonials` array as a prop to the `FancyTestimonialsSlider` component:

```tsx
  export const metadata = {
    title: 'Fancy Testimonials Slider - Cruip Tutorials',
    description: 'Page description',
  }
  
  import TestimonialImg01 from '@/public/testimonial-01.jpg'
  import TestimonialImg02 from '@/public/testimonial-02.jpg'
  import TestimonialImg03 from '@/public/testimonial-03.jpg'
  import FancyTestimonialsSlider from '@/components/fancy-testimonials-slider'
  
  export default function FancyTestimonialSliderPage() {  
  
    const testimonials = [
      {
        img: TestimonialImg01,
        quote: "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.",
        name: 'Jessie J',
        role: 'Acme LTD'
      },
      {
        img: TestimonialImg02,
        quote: "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.",
        name: 'Nick V',
        role: 'Malika Inc.'
      },
      {
        img: TestimonialImg03,
        quote: "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.",
        name: 'Amelia W',
        role: 'Panda AI'
      }
    ]
  
    return (
      <FancyTestimonialsSlider testimonials={testimonials} />
    )
  }
```

We also need to update the `FancyTestimonialsSlider` component to accept props. To do this, the previously defined function as `FancyTestimonialsSlider()` will change to `FancyTestimonialsSlider({ testimonials }: { testimonials: Testimonial[] })`.

Finally, we can remove the imported images from the `FancyTestimonialsSlider` component since they will be passed as props.

Here is the updated complete component:

```tsx
  'use client'
  
  import { useState, useRef, useEffect } from 'react'
  import Image, { StaticImageData } from 'next/image'
  import { Transition } from '@headlessui/react'
  
  interface Testimonial {
    img: StaticImageData
    quote: string
    name: string
    role: string
  }
  
  export default function FancyTestimonialsSlider({ testimonials }: { testimonials: Testimonial[] }) {
    const testimonialsRef = useRef<HTMLDivElement>(null)
    const [active, setActive] = useState<number>(0)
    const [autorotate, setAutorotate] = useState<boolean>(true)
    const autorotateTiming: number = 7000
  
    useEffect(() => {
      if (!autorotate) return
      const interval = setInterval(() => {
        setActive(active + 1 === testimonials.length ? 0 : active => active + 1)
      }, autorotateTiming)
      return () => clearInterval(interval)
    }, [active, autorotate])
  
    const heightFix = () => {
      if (testimonialsRef.current && testimonialsRef.current.parentElement) testimonialsRef.current.parentElement.style.height = `${testimonialsRef.current.clientHeight}px`
    }
  
    useEffect(() => {
      heightFix()
    }, [])  
  
    return (
      <div className="w-full max-w-3xl mx-auto text-center">
        {/* Testimonial image */}
        <div className="relative h-32">
          <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[480px] h-[480px] pointer-events-none before:absolute before:inset-0 before:bg-gradient-to-b before:from-indigo-500/25 before:via-indigo-500/5 before:via-25% before:to-indigo-500/0 before:to-75% before:rounded-full before:-z-10">
            <div className="h-32 [mask-image:_linear-gradient(0deg,transparent,theme(colors.white)_20%,theme(colors.white))]">
  
              {testimonials.map((testimonial, index) => (
                <Transition
                  key={index}
                  show={active === index}
                  className="absolute inset-0 h-full -z-10"
                  enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 order-first"
                  enterFrom="opacity-0 -rotate-[60deg]"
                  enterTo="opacity-100 rotate-0"
                  leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700"
                  leaveFrom="opacity-100 rotate-0"
                  leaveTo="opacity-0 rotate-[60deg]"
                >
                  <Image className="relative top-11 left-1/2 -translate-x-1/2 rounded-full" src={testimonial.img} width={56} height={56} alt={testimonial.name} />
                </Transition>
              ))}
              
            </div>
          </div>
        </div>
        {/* Text */}
        <div className="mb-9 transition-all duration-150 delay-300 ease-in-out">
          <div className="relative flex flex-col" ref={testimonialsRef}>
  
            {testimonials.map((testimonial, index) => (
              <Transition
                key={index}
                show={active === index}
                enter="transition ease-in-out duration-500 delay-200 order-first"
                enterFrom="opacity-0 -translate-x-4"
                enterTo="opacity-100 translate-x-0"
                leave="transition ease-out duration-300 delay-300 absolute"
                leaveFrom="opacity-100 translate-x-0"
                leaveTo="opacity-0 translate-x-4"
                beforeEnter={() => heightFix()}
              >
                <div className="text-2xl font-bold text-slate-900 before:content-['\201C'] after:content-['\201D']">{testimonial.quote}</div>
              </Transition>
            ))}
  
          </div>
        </div>
        {/* Buttons */}
        <div className="flex flex-wrap justify-center -m-1.5">
  
          {testimonials.map((testimonial, index) => (
            <button
              key={index}
              className={`inline-flex justify-center whitespace-nowrap rounded-full px-3 py-1.5 m-1.5 text-xs shadow-sm focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150 ${active === index ? 'bg-indigo-500 text-white shadow-indigo-950/10' : 'bg-white hover:bg-indigo-100 text-slate-900'}`}
              onClick={() => { setActive(index); setAutorotate(false); }}
            >
              <span>{testimonial.name}</span> <span className={`${active === index ? 'text-indigo-200' : 'text-slate-300'}`}>-</span> <span>{testimonial.role}</span>
            </button>
          ))}
  
        </div>
      </div>
    )
  }
```

We hope you enjoyed this tutorial and learned a thing or two about how simple it is to create enjoyable modern components to enrich your [Tailwind CSS landing pages](https://cruip.com/).

If you’re interested in seeing how we developed the same testimonial slider for Alpine.js and Vue, check out the other two parts below: