Skip to main content

Overview

Slide 1
Swipeable carousel content
Carousel displays content in a swipeable, scrollable strip. Built on Embla Carousel, it supports touch/mouse drag, keyboard navigation, autoplay, and loop modes.

Installation

npm install @araf-ds/core embla-carousel-react

Usage

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@araf-ds/core"

export default function Example() {
  return (
    <Carousel className="w-full max-w-sm">
      <CarouselContent>
        {Array.from({ length: 5 }).map((_, i) => (
          <CarouselItem key={i}>
            <Card>
              <CardContent className="flex items-center justify-center p-6">
                <span className="text-4xl font-semibold">{i + 1}</span>
              </CardContent>
            </Card>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}

Variants

Multiple Items Visible

<Carousel opts={{ align: "start" }} className="w-full">
  <CarouselContent className="-ml-2">
    {products.map(product => (
      <CarouselItem key={product.id} className="pl-2 md:basis-1/2 lg:basis-1/3">
        <ProductCard product={product} />
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

With Dots Indicator

const [api, setApi] = useState<CarouselApi>()
const [current, setCurrent] = useState(0)
const [count, setCount] = useState(0)

useEffect(() => {
  if (!api) return
  setCount(api.scrollSnapList().length)
  setCurrent(api.selectedScrollSnap())
  api.on("select", () => setCurrent(api.selectedScrollSnap()))
}, [api])

<div>
  <Carousel setApi={setApi}>
    <CarouselContent>
      {slides.map((slide, i) => (
        <CarouselItem key={i}>{slide}</CarouselItem>
      ))}
    </CarouselContent>
  </Carousel>
  <div className="flex justify-center gap-1.5 mt-3">
    {Array.from({ length: count }).map((_, i) => (
      <button
        key={i}
        onClick={() => api?.scrollTo(i)}
        className={cn(
          "h-2 rounded-full transition-all",
          i === current ? "w-5 bg-primary" : "w-2 bg-muted"
        )}
      />
    ))}
  </div>
</div>

Autoplay

import Autoplay from "embla-carousel-autoplay"

<Carousel
  plugins={[Autoplay({ delay: 3000, stopOnInteraction: true })]}
  opts={{ loop: true }}
>
  <CarouselContent>
    {slides.map((slide, i) => (
      <CarouselItem key={i}>{slide}</CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

API Reference

opts
EmblaOptionsType
Embla Carousel options. Common keys: loop, align, dragFree, slidesToScroll.
plugins
EmblaPluginType[]
Embla plugins array (e.g. Autoplay, AutoScroll).
orientation
string
default:"horizontal"
Scroll direction. Values: horizontal · vertical
setApi
(api: CarouselApi) => void
Callback to access the Embla carousel API for programmatic control.

CarouselItem

className
string
Use Tailwind basis-* classes to control item width (e.g. basis-1/2 for 2 visible items).

Accessibility

  • Carousel uses role="region" with aria-label="carousel"
  • CarouselPrevious and CarouselNext have aria-label="Previous slide" / "Next slide"
  • Keyboard: Arrow Left/Right navigate slides when carousel is focused
  • CarouselItem has role="group" with aria-label="Slide N of M"
  • Autoplay carousels must pause on focus and hover per WCAG 2.1

Do’s & Don’ts

Do

  • Show navigation arrows and dots for discoverability
  • Use opts={{ loop: true }} for image galleries and testimonials
  • Use stopOnInteraction: true with Autoplay so users can read without pressure
  • Set basis-1/2 or basis-1/3 for product card carousels

Don't

  • Don’t autoplay without giving users a way to pause
  • Don’t use Carousel for critical information — users may not see hidden slides
  • Don’t use for navigation — use Tabs or a sidebar instead
  • Don’t make slides too tall — users expect to see the next slide peeking in