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,

  1. heading:(required) - Heading For the Carousel
  2. children:(required) - Elements inside a carousel, It doesn't matter if it's JSX.Element or JSX.Element[]
  3. n_:(optional) - number of elements to be shown , default 3
  4. 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

37