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

Props & Styling

Props

Carouselchildren?: ReactNode (composed of one or more CarouselItem elements).

CarouselItem

PropTypeDefaultDescription
itemReactNode— (required)Content or image rendered inside the slide.
idstring— (required)Stable identifier used to register the item with the parent Carousel.

Styling

CarouselItems register themselves with the parent via context (registerItem/deregisterItem in a mount/unmount useEffect), so Carousel derives the total item count from whoever is actually rendered rather than a prop you have to keep in sync. A ResizeObserver on the track measures itemWidth once, and every item is rendered at that fixed width so the track can slide by simple multiples of it (x: -(activeIndex * itemWidth), 0.3s easeOut).

The non-active items are visually pushed back with scale: 0.95 and a blur(10px) filter (0.2s easeInOut) — this is what communicates "not the current slide" instead of a dimmed overlay or opacity change, and doubles as a subtle depth cue during the slide transition. The prev/next arrows are the shared Button component in its Icon variant, so they inherit the same hover/tap feedback as icon buttons elsewhere in the library.

Code

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