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-react

Copy 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.

Slide 0 of 0
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.