Carousel

A carousel with motion and swipe.

The Carousel component displays a set of slides that can be navigated using previous and next buttons. It supports smooth transitions and can be configured for auto-play. Built from scratch using React and native HTML elements. No UI library dependencies.

Code

TypeScript: Copy this code into components/ui/carousel.tsx:

tsx
"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

interface CarouselContextValue {
  currentIndex: number
  totalSlides: number
  goToSlide: (index: number) => void
  goToPrevious: () => void
  goToNext: () => void
}

const CarouselContext = React.createContext<CarouselContextValue | undefined>(undefined)

const useCarousel = () => {
  const context = React.useContext(CarouselContext)
  if (!context) {
    throw new Error("Carousel components must be used within Carousel")
  }
  return context
}

interface CarouselProps {
  children: React.ReactNode
  className?: string
  autoPlay?: boolean
  interval?: number
}

const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
  ({ children, className, autoPlay = false, interval = 3000 }, ref) => {
    const [currentIndex, setCurrentIndex] = React.useState(0)
    const childrenArray = React.Children.toArray(children)
    const totalSlides = childrenArray.length

    const goToSlide = React.useCallback((index: number) => {
      if (index < 0) {
        setCurrentIndex(totalSlides - 1)
      } else if (index >= totalSlides) {
        setCurrentIndex(0)
      } else {
        setCurrentIndex(index)
      }
    }, [totalSlides])

    const goToPrevious = React.useCallback(() => {
      goToSlide(currentIndex - 1)
    }, [currentIndex, goToSlide])

    const goToNext = React.useCallback(() => {
      goToSlide(currentIndex + 1)
    }, [currentIndex, goToSlide])

    React.useEffect(() => {
      if (autoPlay && totalSlides > 1) {
        const timer = setInterval(() => {
          goToNext()
        }, interval)
        return () => clearInterval(timer)
      }
    }, [autoPlay, interval, goToNext, totalSlides])

    return (
      <CarouselContext.Provider
        value={{
          currentIndex,
          totalSlides,
          goToSlide,
          goToPrevious,
          goToNext,
        }}
      >
        <div ref={ref} className={cn("relative", className)}>
          {children}
        </div>
      </CarouselContext.Provider>
    )
  }
)
Carousel.displayName = "Carousel"

const CarouselContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
  const { currentIndex } = useCarousel()
  const childrenArray = React.Children.toArray(children)

  return (
    <div
      ref={ref}
      className={cn("overflow-hidden", className)}
      {...props}
    >
      <div
        className="flex transition-transform duration-300 ease-in-out"
        style={{
          transform: `translateX(-${currentIndex * 100}%)`,
        }}
      >
        {childrenArray.map((child, index) => (
          <div key={index} className="min-w-full flex-shrink-0">
            {child}
          </div>
        ))}
      </div>
    </div>
  )
})
CarouselContent.displayName = "CarouselContent"

const CarouselItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  return (
    <div
      ref={ref}
      className={cn("w-full", className)}
      {...props}
    />
  )
})
CarouselItem.displayName = "CarouselItem"

const CarouselPrevious = React.forwardRef<
  HTMLButtonElement,
  React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
  const { goToPrevious, totalSlides } = useCarousel()

  if (totalSlides <= 1) return null

  return (
    <button
      ref={ref}
      onClick={goToPrevious}
      className={cn(
        "absolute left-2 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-background hover:bg-muted transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
        className
      )}
      aria-label="Previous slide"
      {...props}
    >
      <svg
        className="h-4 w-4"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M15 19l-7-7 7-7"
        />
      </svg>
    </button>
  )
})
CarouselPrevious.displayName = "CarouselPrevious"

const CarouselNext = React.forwardRef<
  HTMLButtonElement,
  React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
  const { goToNext, totalSlides } = useCarousel()

  if (totalSlides <= 1) return null

  return (
    <button
      ref={ref}
      onClick={goToNext}
      className={cn(
        "absolute right-2 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-background hover:bg-muted transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
        className
      )}
      aria-label="Next slide"
      {...props}
    >
      <svg
        className="h-4 w-4"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M9 5l7 7-7 7"
        />
      </svg>
    </button>
  )
})
CarouselNext.displayName = "CarouselNext"

export {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
}

JavaScript: Copy this code into components/ui/carousel.jsx:

jsx
"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

const CarouselContext = React.createContext(undefined)

const useCarousel = () => {
  const context = React.useContext(CarouselContext)
  if (!context) {
    throw new Error("Carousel components must be used within Carousel")
  }
  return context
}

const Carousel = React.forwardRef(
  ({ children, className, autoPlay = false, interval = 3000 }, ref) => {
    const [currentIndex, setCurrentIndex] = React.useState(0)
    const childrenArray = React.Children.toArray(children)
    const totalSlides = childrenArray.length

    const goToSlide = React.useCallback((index) => {
      if (index < 0) {
        setCurrentIndex(totalSlides - 1)
      } else if (index >= totalSlides) {
        setCurrentIndex(0)
      } else {
        setCurrentIndex(index)
      }
    }, [totalSlides])

    const goToPrevious = React.useCallback(() => {
      goToSlide(currentIndex - 1)
    }, [currentIndex, goToSlide])

    const goToNext = React.useCallback(() => {
      goToSlide(currentIndex + 1)
    }, [currentIndex, goToSlide])

    React.useEffect(() => {
      if (autoPlay && totalSlides > 1) {
        const timer = setInterval(() => {
          goToNext()
        }, interval)
        return () => clearInterval(timer)
      }
    }, [autoPlay, interval, goToNext, totalSlides])

    return (
      <CarouselContext.Provider
        value={{
          currentIndex,
          totalSlides,
          goToSlide,
          goToPrevious,
          goToNext,
        }}
      >
        <div ref={ref} className={cn("relative", className)}>
          {children}
        </div>
      </CarouselContext.Provider>
    )
  }
)
Carousel.displayName = "Carousel"

const CarouselContent = React.forwardRef(
  ({ className, children, ...props }, ref) => {
    const { currentIndex } = useCarousel()
    const childrenArray = React.Children.toArray(children)

    return (
      <div
        ref={ref}
        className={cn("overflow-hidden", className)}
        {...props}
      >
        <div
          className="flex transition-transform duration-200 ease-out will-change-transform"
          style={{
            transform: `translateX(-${currentIndex * 100}%)`,
          }}
        >
          {childrenArray.map((child, index) => (
            <div key={index} className="min-w-full flex-shrink-0">
              {child}
            </div>
          ))}
        </div>
      </div>
    )
  }
)
CarouselContent.displayName = "CarouselContent"

const CarouselItem = React.forwardRef(
  ({ className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn("w-full", className)}
        {...props}
      />
    )
  }
)
CarouselItem.displayName = "CarouselItem"

const CarouselPrevious = React.forwardRef(
  ({ className, ...props }, ref) => {
    const { goToPrevious, totalSlides } = useCarousel()

    if (totalSlides <= 1) return null

    return (
      <button
        ref={ref}
        onClick={goToPrevious}
        className={cn(
          "absolute left-2 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-background hover:bg-muted transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
          className
        )}
        aria-label="Previous slide"
        {...props}
      >
        <svg
          className="h-4 w-4"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M15 19l-7-7 7-7"
          />
        </svg>
      </button>
    )
  }
)
CarouselPrevious.displayName = "CarouselPrevious"

const CarouselNext = React.forwardRef(
  ({ className, ...props }, ref) => {
    const { goToNext, totalSlides } = useCarousel()

    if (totalSlides <= 1) return null

    return (
      <button
        ref={ref}
        onClick={goToNext}
        className={cn(
          "absolute right-2 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-background hover:bg-muted transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
          className
        )}
        aria-label="Next slide"
        {...props}
      >
        <svg
          className="h-4 w-4"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M9 5l7 7-7 7"
          />
        </svg>
      </button>
    )
  }
)
CarouselNext.displayName = "CarouselNext"

export {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
}

Usage

TypeScript:

tsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"

function MyComponent() {
  return (
    <Carousel>
      <CarouselContent>
        <CarouselItem>Slide 1</CarouselItem>
        <CarouselItem>Slide 2</CarouselItem>
        <CarouselItem>Slide 3</CarouselItem>
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}

JavaScript:

jsx
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"

function MyComponent() {
  return (
    <Carousel>
      <CarouselContent>
        <CarouselItem>Slide 1</CarouselItem>
        <CarouselItem>Slide 2</CarouselItem>
        <CarouselItem>Slide 3</CarouselItem>
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}

Make sure you also have the lib/utils.ts file with the cn helper function.

Examples

1
2
3
4
5