How I Created a Custom Carousel In React using useRef and useState in Typescript

Hello world...
This is my first post hoping I can write more. quick intro about me, I have been a developer for 2 years and one year as a Front-End developer, working on react.
I try to create fun react component from scratch rather than relying on a package or some library like material UI or Ant Design. By this post I aim to explain How I created a custom carousel using react hooks some css and typescript.
How It Works:
Before we start I want to show you how it looks in code and how it looks in action
<Carousel
            heading="demo"
            n={2}
            g="12px"
            >
             {[...Array(10)].map((_val,index) => {
                    return(
                        <FlexBoxTitleCard
                        title={`item ${index+1}`}
                        key={`item ${index+1}`}
                        />
                    )
                })}
            </Carousel>
This is How It is in action:
Carousel Demo
CODE:
this is the code for my carousel component we will break it down step by step:
this is how the carousel.tsx looks like
import { useRef, useState } from 'react'

interface CarouselProps {
    heading:string,
    children:JSX.Element[]
    n?:number,
    g?:string,
}

export const Carousel = (props:CarouselProps) => {

    const [active,setActive] = useState(0)

    const carouselRef = useRef<HTMLDivElement>(null)


    const scrollToNextElement= () => {
        if(carouselRef.current){
            if(active < carouselRef.current.childNodes.length - (props.n?props.n:3)){
                carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active + 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft;
                setActive(active +1)
            }
        }
    }

    const scrollToPreviousElement = () => {
        console.log(active);
        if(carouselRef.current){
            if(active > 0) {
                carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active - 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft ;
                setActive(active - 1)
            }
        }
    }

    return(
        <div>
            <div>
                <p>{props.heading}</p>
                <div>
                    <span className="nav-button cursor_pointer" onClick={scrollToPreviousElement} style={{marginRight:"32px"}}>{"<"}</span>
                    <span className="nav-button cursor_pointer" onClick={scrollToNextElement}>{">"}</span>
                </div>
            </div>
            <div
            className="carousel-slides"
            ref={carouselRef}
            style={{
                gridAutoColumns:`calc((100% - (${props.n?props.n:3} - 1)*${props.g?props.g:"32px"})/${props.n?props.n:3})`,
                gridGap:props.g
            }}
            >
                {props.children}
            </div>
        </div>

    )
}
carousel.css
.carousel-slides {
  display: grid;
  grid-auto-flow: column;
  /* grid-auto-columns: calc((100% - (var(--n) - 1) * var(--g)) / var(--n));
    grid-gap: var(--g); */
  overflow: hidden;
  scroll-behavior: smooth;
  padding: 40px 0;
}

.carousel-slides::-webkit-scrollbar {
  display: none;
}

.nav-button {
  padding: 8px;
  border: 1px solid black;
  border-radius: 100%;
}
Carousel will have 4 props:
interface CarouselProps {
    heading:string,
    children:JSX.Element[]
    n?:number,
    g?:string,
}
let's break it down,
  • heading:(required) - Heading For the Carousel
  • children:(required) - Elements inside a carousel, It doesn't matter if it's JSX.Element or JSX.Element[]
  • n_:(optional) - number of elements to be shown , default 3
  • g:(optional) - gap between each carousel element , default 32px
  • const [active,setActive] = useState(0)
    
        const carouselRef = useRef<HTMLDivElement>(null)
    I have a state active which denotes the left most element in the JSX.Element array
    And I have a reference to the carousel element wrapper carouselRef
    const scrollToNextElement= () => {
            if(carouselRef.current){
                if(active < carouselRef.current.childNodes.length - (props.n?props.n:3)){
                    carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active + 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft;
                    setActive(active +1)
                }
            }
        }
    scrollToNextElement function lets you scroll to next element in the carousel. Using carouselRef I am able to access the child nodes and I just change scroll left of the wrapper element to the next elements offset left, it's important substract the parent offsetleft to avoid scrolling the extra padding outside the parent element
    the wrapper element itself is a grid component, the inline styling of the element is done so that it can take in prop n to get how many elements to show inside the carousel
    gridAutoColumns:`calc((100% - (${props.n?props.n:3} - 1)*${props.g?props.g:"32px"})/${props.n?props.n:3})`
    this line of could sets the grid in one single row with as many as n number of items available to see, since the overflow is hidden we cannot see the rest of the elements.
    scrollToPreviousElement uses the same logic
    What Can I do Next:
    add another prop
    step:(optional) - prop which accepts number and will be the numbers of elements it will skip when we click next or previous
    Scrape both scrollToNextElement and scrollToPreviousElement and change to scrollToElement which accepts a number and skips to that element

    43

    This website collects cookies to deliver better user experience

    How I Created a Custom Carousel In React using useRef and useState in Typescript