Carousel

The carousel was one of the initial components I started on. As I went through the journey of creating this design system, I found it was better for the items to have a react node typing so that the carousel items can take in either content or images depending on the developer’s preference. In future versions, I plan to have variations for the current version and a version that takes up the full width and height of the parent container.

Slide One
Slide Two
Slide Three

Code

// Carousel.tsx
import Button from "@components/Button";
import { buttonVariant } from "@types";
import { ArrowLeft, ArrowRight } from "lucide-react";
import CarouselContent from "./CarouselContent";
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
import React from "react";
import { CarouselContext } from "./CarouselContext";

interface CarouselProps {
    children?: React.ReactNode
}

const Carousel = ({children}: CarouselProps) => {
  const [activeIndex, setActiveIndex] = useState(0);
  const [itemIds, setItemIds] = useState<string[]>([]);
  const [itemWidth, setItemWidth] = useState(0)
  const widthRef = useRef<HTMLDivElement>(null);

  const handleLeftArrow = () => {
        setActiveIndex(Math.max(activeIndex - 1, 0))  
  }

  const handleRightArrow = () => {
        setActiveIndex(Math.min(activeIndex + 1, totalItems - 1)) 
  }

  const totalItems = itemIds.length;

   const registerItem = useCallback((newItem: string) => {
    setItemIds(prev => {
        if(!prev.includes(newItem)) {
            return [...prev, newItem]   
        }

        else {
            return prev
        }
    })
  },[]);

  const deregisterItem = useCallback((chosenItem: string) => {
        setItemIds(prev => {
            return prev.filter((item) => item !== chosenItem)
        })
  }, [])

  const contextValues = useMemo(() => {
        return {
            activeIndex,
            setActiveIndex,
            registerItem,
            deregisterItem,
            itemWidth,
            itemIds,
        }
  }, [activeIndex, setActiveIndex, registerItem, deregisterItem, itemWidth, itemIds,])

  useEffect(() => {
        const observer = new ResizeObserver((entries) => {      
        const newWidth = entries[0].contentRect.width;
        if (newWidth > 0 && newWidth < 5000) {
            setItemWidth(newWidth);
            }
        })
        if(widthRef.current){
            observer.observe(widthRef.current)
        }
        return () => observer.disconnect()
  },[])

  return (
    <CarouselContext value = {contextValues}>
        <div className = "grid grid-cols-[auto_1fr_auto] gap-2 justify-center items-center w-full h-full relative overflow-hidden"
        >
        <Button
        variant = {buttonVariant.Icon}
        onClick={handleLeftArrow}
        >
            <ArrowLeft className = "w-4 h-4 text-gray-600"></ArrowLeft>
        </Button>
             <div ref = {widthRef} className = "h-full self-stretch overflow-hidden ">
            <CarouselContent
            >
            {children}
        </CarouselContent>
        </div>
        <Button
        variant = {buttonVariant.Icon}
        onClick={handleRightArrow}
        >
            <ArrowRight className = "w-4 h-4 text-gray-600"></ArrowRight>
        </Button>
    </div>
    </CarouselContext>
  )
}

export default Carousel

// CarouselContent.tsx
import React from 'react'
import { useContext } from 'react';
import { CarouselContext } from './CarouselContext';
import { motion } from 'motion/react';

interface CarouselContentProps {
    children: React.ReactNode
}

const CarouselContent = ({children}: CarouselContentProps) => {

  const context = useContext(CarouselContext)
  if(!context) return null;

  return (
    <div 
    className = "h-full min-w-0 overflow-hidden">
        <motion.div 
        animate = {{x: -(context.activeIndex * context.itemWidth)}}
        transition = {context.itemWidth === 0 ? { duration: 0 } : { ease: 'easeOut', duration: 0.3 }}
        className = "flex flex-row h-full">
            {children}
        </motion.div>
    </div>
  )
}

export default CarouselContent

// CarouselItem.tsx
import { useContext, useId, useEffect } from 'react';
import type { ReactNode } from 'react';
import { CarouselContext } from './CarouselContext';
import { motion } from 'motion/react';

interface CarouselItemProps {
    item: ReactNode,
    id: string;
}

const CarouselItem = ({item, id}: CarouselItemProps) => {

  const context = useContext(CarouselContext);
  if(!context) return null;

  const generatedId = useId();
  const itemId = id || generatedId

  const myIndex = context.itemIds.indexOf(itemId)

  useEffect(() => {
    context.registerItem(itemId)
    return(() => context.deregisterItem(itemId))
  }, [])

  return (
    <motion.div 
    id = {itemId}
    animate = {myIndex === context.activeIndex ? {scale: 1 ,filter: 'blur(0px)'} : {scale: 0.95,filter: 'blur(10px)'}}
    transition = {{ease: 'easeInOut', duration: 0.2}}
    style={{width: context.itemWidth, minWidth: context.itemWidth}}
    className = "flex items-center justify-center h-full p-6 border border-gray-300 rounded-md sm:p-12 ">
            {item}
        </motion.div>
  )
}

export default CarouselItem