Email

React Compatible Flickity Carousel

A simple powerful Carousel, powered by flickity.

npm install --save flickity@2.3.0 react-flickity-component lucide-react clsx tailwind-merge tailwind

This command installs several packages and adds them as dependencies in your package.json file. Each of these packages may seem a bit random, but to enable you to copy and paste it into your repo you'll need them for different parts of the code, otherwise you can simply install

and strip down my code for your usage.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

A helper function to handle classnames

Next we need to create a helper function to allow us to dynamically merge classnames, there's about a million versions of this but seeing as I am using ShadCN I have just integrated theres as appropriate and it works perfectly.

Combining clsx with tailwind-merge in the cn function provides a streamlined approach to handle dynamic and conflicting class names in Tailwind CSS projects.

"use client"

import React, { useState, useEffect, useRef } from "react"
import Flickity from "react-flickity-component"
import { ArrowLeft, ArrowRight } from "lucide-react"
import "flickity/css/flickity.css"
import { cn } from "@/lib/utils"

type TCarousel = {
  children: React.ReactNode
  options?: any
}

type TButton = {
  onClick: () => void
  children: React.ReactNode
  className?: string
}

const CarouselButton = ({ onClick, children, className }: TButton) => {
  return (
    <button
      onClick={onClick}
      className={cn(
        "shadow-[0px_0px_5px_0px_rgba(0,0,0,0.75)] absolute top-1/2 -translate-y-1/2 opacity-50 hover:opacity-100 bg-white rounded-full p-2 ease-in-out duration-200",
        className
      )}
    >
      {children}
    </button>
  )
}

const Carousel: React.FC<TCarousel> = ({ children, options }) => {
  const ref: any = useRef(null)
  const [loaded, setLoaded] = useState<boolean>(false)
  const [visisble, setVisisble] = useState<boolean>(false)

  const slideNext = () => {
    if (!ref.current) return
    ref.current.next()
  }

  const slidePrev = () => {
    if (!ref.current) return
    ref.current.previous()
  }

  useEffect(() => {
    if (!loaded || !ref.current) return

    const handleSettle = () => {
      setVisisble(true)
    }

    ref.current.on("ready", handleSettle)

    return () => {
      ref.current?.off("ready", handleSettle)
    }
  }, [loaded])

  const flickityOptions: any = {
    prevNextButtons: false,
    ...options,
  }

  return (
    <>
      <div className="relative">
        <div
          role="status"
          className={cn(
            "space-y-8 container  md:space-y-0 md:space-x-8 rtl:space-x-reverse flex flex-row items-center justify-center",
            !visisble && "opacity-100 height-auto",
            visisble && "opacity-0 height-0 hidden"
          )}
        >
          <div className="flex animate-pulse items-center justify-center w-full h-[400px] bg-gray-300 rounded-sm w-full dark:bg-gray-800">
            <svg
              className="w-10 h-10 text-gray-200 dark:text-gray-600"
              aria-hidden="true"
              xmlns="http://www.w3.org/2000/svg"
              fill="currentColor"
              viewBox="0 0 20 18"
            >
              <path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z" />
            </svg>
          </div>
        </div>

        <div className={cn({ "height-auto": visisble, hidden: !visisble })}>
          
            <Flickity
              flickityRef={(c) => {
                if (c) {
                  ref.current = c as unknown as any
                  setLoaded(true)
                }
              }}
              className="carousel"
              elementType="div"
              options={flickityOptions}
              disableImagesLoaded={false}
              reloadOnUpdate
              static
            >
              {children}
            </Flickity>
        </div>
        <div className="container absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
          <CarouselButton onClick={slidePrev} className="left-4">
            <ArrowLeft color="var(--brand-secondary)" size={30} />
          </CarouselButton>
          <CarouselButton onClick={slideNext} className="right-4">
            <ArrowRight color="var(--brand-secondary)" size={30} />
          </CarouselButton>
        </div>
      </div>
    </>
  )
}

export default Carousel

In this implementation, we import essential helper classes and components to streamline our development process. We assign a reference to the carousel instance and use the setLoaded function to confirm its initialization. Once initialized, we can attach custom events or methods to the carousel as needed.​

To maintain code simplicity and avoid repetition, we've created a reusable button component. This component utilizes icons from the Lucide library, which offers a wide range of customizable, scalable icons suitable for React projects . Additionally, we've incorporated a skeleton loader to enhance user experience during the carousel's loading phase.

import { type ReactNode } from "react"
import { Carousel } from "@/components/carousel"

const CarouselSection = ({ ...data }: any): ReactNode => {
  return (
    <div className="w-full carousel">
      <Carousel options={{ wrapAround: false, adaptiveHeight: true }}>
        <div>a</div>
        <div>b</div>
        <div>c</div>
      </Carousel>        
    </div>
  )
}

export default CarouselSection

And this is a basic example of how we use the component itself, remember we have created a reusable base, so you can either dump it in directly if it's a simple implementation, or include this into a larger composite component if you're visual layout requirements or instrumentation are a bit more involved.

Get in touch

I am always free to discuss new projects, opportunities or any assistance you may require.

Responses usually take less than 24 hours.