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.