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