Overview

aSaaSin wraps Embla Carousel with a small React context so you get a clean API, themed controls, and accessibility defaults. Use it for logo scrollers, product cards, or hero showcases. Base API is similar to the shadcn/ui Carousel and the engine is provided by Embla Carousel.

Exports

From @/components/ui/Carousel:

  • Carousel - root that wires Embla and keyboard navigation
  • CarouselContent - scroll container
  • CarouselItem - slide item
  • CarouselPrevious · CarouselNext - themed arrow buttons
  • useCarouselDots(api) - helper hook for dot indicators

Props on Carousel:

  • opts - Embla options like align, loop, containScroll
  • plugins - Embla plugins array
  • orientation - 'horizontal' | 'vertical' (default 'horizontal')
  • setApi - get the Embla API instance when ready

Basic usage

A minimal horizontal carousel with three slides. Items use responsive basis widths so the layout adapts to breakpoints.

'use client';

import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/Carousel';

export function BasicCarousel() {
  return (
    <div className="relative mx-auto w-full max-w-5xl">
      <Carousel
        opts={{
          align: 'start',
          containScroll: 'trimSnaps',
          loop: false,
        }}
      >
        <CarouselContent className="pl-2 sm:pl-0">
          {['One', 'Two', 'Three'].map((label) => (
            <CarouselItem
              key={label}
              className="max-w-[18rem] basis-[80%] sm:basis-1/2 md:basis-1/3"
            >
              <div className="h-40 rounded-2xl bg-secondary-light p-6 dark:bg-primary-dark">
                {label}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>

        <div className="mt-4 hidden w-full justify-end sm:flex">
          <div className="flex gap-4">
            <CarouselPrevious className="static translate-y-0" />
            <CarouselNext className="static translate-y-0" />
          </div>
        </div>
      </Carousel>
    </div>
  );
}

Dots

Use setApi to grab the Embla API and feed it to useCarouselDots. The hook returns the current index, snap list, and a scrollTo handler.

'use client';

import * as React from 'react';
import { Carousel, CarouselContent, CarouselItem, useCarouselDots } from '@/components/ui/Carousel';
import { cn } from '@/utils';

export function DotsExample() {
  const [api, setApi] = React.useState<any>(null);
  const { selectedIndex, scrollSnaps, scrollTo } = useCarouselDots(api);

  return (
    <Carousel setApi={setApi} opts={{ align: 'center', containScroll: 'trimSnaps' }}>
      <CarouselContent className="pl-2">
        {Array.from({ length: 5 }).map((_, i) => (
          <CarouselItem key={i} className="basis-[85%] sm:basis-1/2 md:basis-1/3">
            <div className="h-36 rounded-2xl bg-secondary-light p-6 dark:bg-primary-dark">
              Slide {i + 1}
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>

      <div className="mt-3 flex justify-center gap-2 sm:hidden">
        {scrollSnaps.map((_, idx) => (
          <button
            key={idx}
            aria-label={`Go to slide ${idx + 1}`}
            onClick={() => scrollTo(idx)}
            className={cn(
              'h-2 w-2 rounded-full shadow-sm transition-colors',
              idx === selectedIndex
                ? 'bg-accent'
                : 'bg-secondary-dark/50 dark:bg-neutral/50',
            )}
          />
        ))}
      </div>
    </Carousel>
  );
}

Orientation

Switch to vertical with orientation="vertical". Arrows rotate and items stack.

'use client';

import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/Carousel';

export function VerticalCarousel() {
  return (
    <div className="relative mx-auto h-96 max-w-md">
      <Carousel orientation="vertical" opts={{ align: 'start' }}>
        <CarouselContent>
          {['A', 'B', 'C', 'D'].map((label) => (
            <CarouselItem key={label}>
              <div className="h-24 rounded-2xl bg-secondary-light p-4 dark:bg-primary-dark">
                {label}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>

        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
    </div>
  );
}

Accessibility

The root sets role="region" and aria-roledescription="carousel". Arrow keys work on the container so users can navigate with the keyboard. Buttons have screen reader text. Keep slide content accessible and ensure color contrast on controls.

Tips

  • Use containScroll: 'trimSnaps' to avoid partial slides at the ends
  • Keep arrows visible on desktop and show dots on mobile for quick orientation
  • Prefer fixed heights for predictable layouts and smoother GPU transforms
  • For autoplay, add the Embla autoplay plugin in plugins and pause on pointer or focus