Carousel
A carousel with motion and swipe built using Embla.
About
The carousel component is built using the Embla Carousel library.
Installation
Install the following dependencies:
npm install embla-carousel-reactCopy and paste the following code into your project.
'use client';
import clsx from 'clsx';
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import {
ComponentProps,
HTMLAttributes,
KeyboardEvent,
createContext,
forwardRef,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { Button } from '../button';
import styles from './styles.module.scss';
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />');
}
return context;
}
const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & CarouselProps>(
({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const onSelect = useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
);
useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on('reInit', onSelect);
api.on('select', onSelect);
return () => {
api?.off('select', onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={clsx(styles.carousel, className)}
role='region'
aria-roledescription='carousel'
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
);
Carousel.displayName = 'Carousel';
const CarouselContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className={styles.carousel__content__wrapper}>
<div
ref={ref}
className={clsx(
styles.carousel__content,
orientation === 'horizontal'
? styles.carousel__content__horizontal
: styles.carousel__content__vertical,
className
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = 'CarouselContent';
const CarouselItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role='group'
aria-roledescription='slide'
className={clsx(
styles.carousel__item,
orientation === 'horizontal' ? styles.carousel__item__horizontal : styles.carousel__item__vertical,
className
)}
{...props}
/>
);
});
CarouselItem.displayName = 'CarouselItem';
const CarouselPrevious = forwardRef<HTMLButtonElement, ComponentProps<typeof Button>>(
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={clsx(
styles.carousel__previous,
orientation === 'horizontal'
? styles.carousel__previous__horizontal
: styles.carousel__previous__vertical,
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className={styles.icon} />
<span className={styles.sr_only}>Previous slide</span>
</Button>
);
}
);
CarouselPrevious.displayName = 'CarouselPrevious';
const CarouselNext = forwardRef<HTMLButtonElement, ComponentProps<typeof Button>>(
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={clsx(
styles.carousel__next,
orientation === 'horizontal' ? styles.carousel__next__horizontal : styles.carousel__next__vertical,
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className={styles.icon} />
<span className={styles.sr_only}>Next slide</span>
</Button>
);
}
);
CarouselNext.displayName = 'CarouselNext';
export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi };Update the import paths to match your project setup.
Usage
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';<Carousel>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>Examples
Sizes
To set the size of the items, you can use the flex-basis utility class on the <CarouselItem />.
Spacing
To set the spacing between the items, we use a padding-left utility on the <CarouselItem /> and a negative margin-left on the <CarouselContent />.
Orientation
Use the orientation prop to set the orientation of the carousel.
<Carousel orientation='vertical | horizontal'>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>Options
You can pass options to the carousel using the opts prop. See the Embla Carousel docs for more information.
<Carousel
opts={{
align: 'start',
loop: true,
}}
>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>API
Use a state and the setApi props to get an instance of the carousel API.
import { type CarouselApi } from '@/components/ui/carousel';
export function Example() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [count, setCount] = React.useState(0);
React.useEffect(() => {
if (!api) {
return;
}
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
api.on('select', () => {
setCurrent(api.selectedScrollSnap() + 1);
});
}, [api]);
return (
<Carousel setApi={setApi}>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>
);
}Events
You can listen to events using the api instance from setApi.
import { type CarouselApi } from '@/components/ui/carousel';
export function Example() {
const [api, setApi] = React.useState<CarouselApi>();
React.useEffect(() => {
if (!api) {
return;
}
api.on('select', () => {
// Do something on select.
});
}, [api]);
return (
<Carousel setApi={setApi}>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>
);
}See the Embla Carousel docs for more information on using events.
Plugins
You can use the plugins prop to add plugins to the carousel.
import Autoplay from "embla-carousel-autoplay"
export function Example() {
return (
<Carousel
plugins={[
Autoplay({
delay: 2000,
}),
]}
>
// ...
</Carousel>
)
}See the Embla Carousel docs for more information on using plugins.