Carousel
A carousel with motion and swipe built using Embla.
1
2
3
4
5
tsx
import * as React from "react"
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel"
export function CarouselDemo() { return ( <Carousel className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <div className="p-1"> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{index + 1}</span> </CardContent> </Card> </div> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> )}
Installation
CLI
bash
npx fivui add carousel
Manual
Install the following dependencies:
bash
npm install embla-carousel-react
For autoplay functionality, also install:
bash
npm install embla-carousel-autoplay
Copy and paste the following code into your project:
tsx
"use client"
import * as React from "react"import useEmblaCarousel, { type UseEmblaCarouselType,} from "embla-carousel-react"import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"import { Button } from "@/components/ui/button"
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 = React.createContext<CarouselContextProps | null>(null)
function useCarousel() { const context = React.useContext(CarouselContext)
if (!context) { throw new Error("useCarousel must be used within a <Carousel />") }
return context}
function Carousel({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props}: React.ComponentProps<"div"> & CarouselProps) { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, plugins ) const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => { if (!api) return setCanScrollPrev(api.canScrollPrev()) setCanScrollNext(api.canScrollNext()) }, [])
const scrollPrev = React.useCallback(() => { api?.scrollPrev() }, [api])
const scrollNext = React.useCallback(() => { api?.scrollNext() }, [api])
const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === "ArrowLeft") { event.preventDefault() scrollPrev() } else if (event.key === "ArrowRight") { event.preventDefault() scrollNext() } }, [scrollPrev, scrollNext] )
React.useEffect(() => { if (!api || !setApi) return setApi(api) }, [api, setApi])
React.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 onKeyDownCapture={handleKeyDown} className={cn("relative", className)} role="region" aria-roledescription="carousel" {...props} > {children} </div> </CarouselContext.Provider> )}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { const { carouselRef, orientation } = useCarousel()
return ( <div ref={carouselRef} className="overflow-hidden"> <div className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className )} {...props} /> </div> )}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { const { orientation } = useCarousel()
return ( <div role="group" aria-roledescription="slide" className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className )} {...props} /> )}
function CarouselPrevious({ className, variant = "outline", size = "icon", ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return ( <Button variant={variant} size={size} className={cn( "absolute size-8 rounded-full", orientation === "horizontal" ? "top-1/2 -left-12 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className )} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > <ArrowLeft /> <span className="sr-only">Previous slide</span> </Button> )}
function CarouselNext({ className, variant = "outline", size = "icon", ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollNext, canScrollNext } = useCarousel()
return ( <Button variant={variant} size={size} className={cn( "absolute size-8 rounded-full", orientation === "horizontal" ? "top-1/2 -right-12 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className )} disabled={!canScrollNext} onClick={scrollNext} {...props} > <ArrowRight /> <span className="sr-only">Next slide</span> </Button> )}
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext,}
Usage
tsx
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel"
tsx
<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 basis
utility class on the CarouselItem
.
1
2
3
4
5
tsx
<Carousel opts={{ align: "start", }} className="w-full max-w-sm"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3"> <div className="p-1"> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-3xl font-semibold">{index + 1}</span> </CardContent> </Card> </div> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /></Carousel>
Orientation
Use the orientation
prop to set the orientation of the carousel.
1
2
3
4
5
tsx
<Carousel opts={{ align: "start", }} orientation="vertical" className="w-full max-w-xs"> <CarouselContent className="-mt-1 h-[200px]"> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index} className="pt-1 md:basis-1/2"> <div className="p-1"> <Card> <CardContent className="flex items-center justify-center p-6"> <span className="text-3xl font-semibold">{index + 1}</span> </CardContent> </Card> </div> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /></Carousel>
API
Use a state and the setApi
prop to get an instance of the carousel API.
1
2
3
4
5
Slide 0 of 0
tsx
"use client"
import * as React from "react"import { type CarouselApi } from "@/components/ui/carousel"
export function CarouselApiDemo() { 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 ( <div className="mx-auto max-w-xs"> <Carousel setApi={setApi} className="w-full max-w-xs"> <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{index + 1}</span> </CardContent> </Card> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> <div className="text-muted-foreground py-2 text-center text-sm"> Slide {current} of {count} </div> </div> )}
Plugins
You can use the plugins
prop to add plugins to the carousel.
1
2
3
4
5
tsx
"use client"
import * as React from "react"import Autoplay from "embla-carousel-autoplay"
import { Card, CardContent } from "@/components/ui/card"import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel"
export function CarouselPlugin() { const plugin = React.useRef( Autoplay({ delay: 2000, stopOnInteraction: true }) )
return ( <Carousel plugins={[plugin.current]} className="w-full max-w-xs" onMouseEnter={plugin.current.stop} onMouseLeave={plugin.current.reset} > <CarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <CarouselItem key={index}> <div className="p-1"> <Card> <CardContent className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{index + 1}</span> </CardContent> </Card> </div> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> </Carousel> )}
API Reference
Carousel
Prop | Type | Default | Description |
---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | The orientation of the carousel. |
opts | CarouselOptions | - | Options to pass to the Embla Carousel. |
plugins | CarouselPlugin | - | Plugins to pass to the Embla Carousel. |
setApi | (api: CarouselApi) => void | - | Callback to get the carousel API instance. |
CarouselContent
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes for the carousel content. |
CarouselItem
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes for the carousel item. |
CarouselPrevious
Prop | Type | Default | Description |
---|---|---|---|
variant | ButtonVariant | "outline" | The button variant for the previous button. |
size | ButtonSize | "icon" | The button size for the previous button. |
CarouselNext
Prop | Type | Default | Description |
---|---|---|---|
variant | ButtonVariant | "outline" | The button variant for the next button. |
size | ButtonSize | "icon" | The button size for the next button. |