Carousel

Horizontal or vertical slides with controls and swipe support.

Build swipeable galleries on Embla. Compose the slides, then place previous and next controls beside the track or in a row below it.

1
2
3
4
5
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselItem,  CarouselNext,  CarouselPrevious,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel 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-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselPrevious className="absolute -left-12 top-1/2 -translate-y-1/2" />      <CarouselNext className="absolute -right-12 top-1/2 -translate-y-1/2" />    </Carousel>  )}

Installation

npx @arctis-sh/@arctis-sh/ui@latest add carousel

Installs with Button and requires the embla-carousel-react peer dependency. The plugins example also uses embla-carousel-autoplay.

Usage

import {  Carousel,  CarouselContent,  CarouselItem,  CarouselNext,  CarouselPrevious,} from "@/components/ui/carousel"
<Carousel>  <CarouselContent>    <CarouselItem>...</CarouselItem>    <CarouselItem>...</CarouselItem>    <CarouselItem>...</CarouselItem>  </CarouselContent>  <CarouselPrevious className="absolute -left-12 top-1/2 -translate-y-1/2" />  <CarouselNext className="absolute -right-12 top-1/2 -translate-y-1/2" /></Carousel>

Composition

Carousel
├── CarouselContent
│   ├── CarouselItem
│   └── CarouselItem
├── CarouselPrevious
├── CarouselNext
├── CarouselControls
└── CarouselStatus

Controls

CarouselControls places previous and next controls below the track.

1
2
3
4
5
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel 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-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls />    </Carousel>  )}

Pass showStatus to display Slide N of M between them with CarouselStatus.

1
2
3
4
5
Slide 0 of 0
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel 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-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls showStatus />    </Carousel>  )}

Loop

Set opts.loop to continue smoothly from the last slide to the first.

1
2
3
4
5
Slide 0 of 0
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel opts={{ loop: true }} 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-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls showStatus />    </Carousel>  )}

Sizes

Set a basis on CarouselItem to change slide width. Use opts.align: "start" when the frame shows more than one slide.

1
2
3
4
5
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel opts={{ align: "start" }} className="w-full max-w-sm">      <CarouselContent>        {Array.from({ length: 5 }).map((_, index) => (          <CarouselItem key={index} className="basis-1/3">            <Card>              <CardContent className="flex aspect-square items-center justify-center p-4">                <span className="text-2xl font-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls />    </Carousel>  )}
<CarouselItem className="basis-1/3">...</CarouselItem><CarouselItem className="md:basis-1/2 lg:basis-1/3">...</CarouselItem>

Spacing

Create space between slides with matching -ml-[VALUE] on CarouselContent and pl-[VALUE] on each CarouselItem.

1
2
3
4
5
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel opts={{ align: "start" }} className="w-full max-w-sm">      <CarouselContent className="-ml-8">        {Array.from({ length: 5 }).map((_, index) => (          <CarouselItem key={index} className="basis-1/2 pl-8">            <Card>              <CardContent className="flex aspect-square items-center justify-center p-4">                <span className="text-2xl font-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls />    </Carousel>  )}
<CarouselContent className="-ml-8">  <CarouselItem className="basis-1/2 pl-8">...</CarouselItem></CarouselContent>

Orientation

Set orientation="vertical" for a vertical track. Give CarouselContent a fixed height and use basis-full to fill the frame with one slide.

1
2
3
4
5
tsx
import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel      opts={{ align: "start" }}      orientation="vertical"      className="w-full"    >      <CarouselContent className="h-[220px]">        {Array.from({ length: 5 }).map((_, index) => (          <CarouselItem key={index} className="basis-full pt-0">            <Card className="h-full">              <CardContent className="flex h-full items-center justify-center p-6">                <span className="text-3xl font-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls />    </Carousel>  )}

Options

Pass Embla options directly through opts.

<Carousel  opts={{    align: "start",    loop: true,  }}>  <CarouselContent>    <CarouselItem>...</CarouselItem>  </CarouselContent></Carousel>

API

Use setApi to keep the Embla API in state for slide indexes, counts, and custom controls.

1
2
3
4
5

Slide 0 of 0

tsx
import { useEffect, useState } from "react"import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,  type CarouselApi,} from "@/components/ui/carousel"export function Example() {  const [api, setApi] = useState<CarouselApi>()  const [current, setCurrent] = useState(0)  const [count, setCount] = useState(0)  useEffect(() => {    if (!api) return    setCount(api.scrollSnapList().length)    setCurrent(api.selectedScrollSnap() + 1)    api.on("select", () => {      setCurrent(api.selectedScrollSnap() + 1)    })  }, [api])  return (    <div className="w-full max-w-xs">      <Carousel setApi={setApi}>        <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-normal tracking-wide">                    {index + 1}                  </span>                </CardContent>              </Card>            </CarouselItem>          ))}        </CarouselContent>        <CarouselControls />      </Carousel>      <p className="mt-2 text-center text-sm text-muted-foreground">        Slide {current} of {count}      </p>    </div>  )}
import { useEffect, useState } from "react"import { type CarouselApi } from "@/components/ui/carousel"const [api, setApi] = useState<CarouselApi>()const [current, setCurrent] = useState(0)const [count, setCount] = useState(0)useEffect(() => {  if (!api) return  setCount(api.scrollSnapList().length)  setCurrent(api.selectedScrollSnap() + 1)  api.on("select", () => {    setCurrent(api.selectedScrollSnap() + 1)  })}, [api])return <Carousel setApi={setApi}>...</Carousel>

Events

Subscribe to events on the API returned through setApi. Embla documents the full event list.

useEffect(() => {  if (!api) return  api.on("select", () => {    // Do something on select.  })}, [api])

Plugins

Pass Embla plugins through the plugins prop.

1
2
3
4
5
tsx
import Autoplay from "embla-carousel-autoplay"import { Card, CardContent } from "@/components/ui/card"import {  Carousel,  CarouselContent,  CarouselControls,  CarouselItem,} from "@/components/ui/carousel"export function Example() {  return (    <Carousel      plugins={[        Autoplay({          delay: 2000,        }),      ]}      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-normal tracking-wide">                  {index + 1}                </span>              </CardContent>            </Card>          </CarouselItem>        ))}      </CarouselContent>      <CarouselControls />    </Carousel>  )}
import Autoplay from "embla-carousel-autoplay"<Carousel  plugins={[    Autoplay({      delay: 2000,    }),  ]}>  ...</Carousel>

API Reference

The root container that provides carousel context to its children.

PropTypeDefault
orientation"horizontal" | "vertical""horizontal"
optsCarouselOptions
pluginsCarouselPlugin
setApi(api: CarouselApi) => void
classNamestring

opts and plugins are Embla types from embla-carousel-react.

<Carousel>  <CarouselContent>    <CarouselItem>Slide 1</CarouselItem>  </CarouselContent>  <CarouselPrevious />  <CarouselNext /></Carousel>

CarouselContent

Contains the viewport and slide track. Put horizontal spacing classes such as -ml-* here, or height classes for a vertical carousel.

PropTypeDefault
classNamestring
<CarouselContent>  <CarouselItem>Slide 1</CarouselItem></CarouselContent>

CarouselItem

A single slide, full width by default with basis-full. Use another basis-* value for multi-slide layouts.

PropTypeDefault
classNamestring
<CarouselItem>Slide 1</CarouselItem>

CarouselPrevious

The previous control, built on Button. It disables when there is nowhere to scroll back.

PropTypeDefault
variantButtonVariant"outline"
sizeButtonSize"icon-sm"
classNamestring
<CarouselPrevious />

CarouselNext

The next control, built on Button. It disables when there is nowhere to scroll forward.

PropTypeDefault
variantButtonVariant"outline"
sizeButtonSize"icon-sm"
classNamestring
<CarouselNext />

CarouselControls

A bottom row for previous and next controls, with an optional status in the center.

PropTypeDefault
showStatusbooleanfalse
classNamestring
<CarouselControls>  <CarouselPrevious />  <CarouselNext /></CarouselControls>

CarouselStatus

Renders Slide N of M from the Embla API. It is usually enabled with CarouselControls showStatus.

PropTypeDefault
classNamestring
<CarouselStatus />

CarouselApi

Type alias for the Embla API returned by setApi. Use it for methods such as selectedScrollSnap, scrollSnapList, scrollNext, and scrollPrev, plus events such as select.

const [api, setApi] = useState<CarouselApi>()<Carousel setApi={setApi}>  ...</Carousel>