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 navigationCarouselContent
- scroll containerCarouselItem
- slide itemCarouselPrevious
·CarouselNext
- themed arrow buttonsuseCarouselDots(api)
- helper hook for dot indicators
Props on Carousel
:
opts
- Embla options likealign
,loop
,containScroll
plugins
- Embla plugins arrayorientation
-'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