Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/carousel.jsonInstalls to @/components/ui/carousel.
Manual installation
Install the dependencies:
pnpm add embla-carousel-react@^8.6.0 lucide-react@^1.48.0Add the components it builds on: button.
Copy the source into your project:
'use client'
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
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])
const contextValue = React.useMemo<CarouselContextProps>(
() => ({
carouselRef,
api,
opts,
orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}),
[carouselRef, api, opts, orientation, scrollPrev, scrollNext, canScrollPrev, canScrollNext],
)
return (
<CarouselContext.Provider value={contextValue}>
<div
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
<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"
data-slot="carousel-item"
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-sm',
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
'absolute touch-manipulation rounded-full',
orientation === 'horizontal' ? 'inset-y-0 -left-12 my-auto' : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ChevronLeftIcon />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = 'outline',
size = 'icon-sm',
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
'absolute touch-manipulation rounded-full',
orientation === 'horizontal' ? 'inset-y-0 -right-12 my-auto' : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ChevronRightIcon />
<span className="sr-only">Next slide</span>
</Button>
)
}
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, useCarousel }A carousel with motion and swipe built on Embla Carousel. It supports horizontal and vertical orientation, several items per view, looping and programmatic control through the Embla API, and is suited for news, highlights and image galleries.
Features
- Horizontal and vertical orientation
- Swipe, drag and keyboard (arrow key) navigation
- Configurable item sizes and spacing via basis and padding classes
- Embla options such as loop and align
- Access to the Embla API through setApi
- Plugin support (e.g. autoplay)
Usage
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel'
export function NewsCarousel() {
return (
<Carousel aria-label="News" className="w-full max-w-xs">
<CarouselContent>
<CarouselItem>New recycling station opens in Norrby</CarouselItem>
<CarouselItem>Apply for a summer job before 15 April</CarouselItem>
<CarouselItem>Road works on Storgatan until August</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}Examples
Default
One news card per slide with previous and next buttons. The examples use token-based placeholders where your site would show photos.
Multiple Items
Several items per view using basis-* classes on CarouselItem, here upcoming events.
Spacing
Tighter spacing: a negative margin on CarouselContent (-ml-2) matched by padding on each item (pl-2).
Vertical
orientation="vertical" with two service notices per view. Give CarouselContent a fixed height.
Loop
opts={{ loop: true }} goes back to the first photo after the last.
With Api
setApi gives access to the Embla API, used here for step dots that show and change the current step of a guide.
Accessibility
- The root has role="region" and aria-roledescription="carousel"
- Each item has role="group" and aria-roledescription="slide"
- Arrow keys move between slides when the carousel has focus
- Previous and next buttons include screen reader labels and are disabled at the edges
- Avoid autoplay, or provide a way to pause it