Skip to content

Carousel

Stable

A horizontally or vertically scrolling set of slides with swipe, keyboard and button navigation, built on Embla.

TokensOne source of truth
ComponentsAccessible by default
PatternsProven compositions
ThemesBring your own brand

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/carousel.json

Usage

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"
<Carousel aria-label="Product screenshots">
  <CarouselContent>
    <CarouselItem></CarouselItem>
    <CarouselItem></CarouselItem>
    <CarouselItem></CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

The previous and next buttons sit outside the slides, 48px to the left and right. Leave room for them — or position them yourself with className.

Examples

Multiple per view

Set the slide width with a basis-* class on CarouselItem. Combine it with responsive prefixes to show more slides on wider screens, and opts={{ align: "start" }} to snap slides to the leading edge.

InvoiceLine items, tax and totals
ChangelogDated entries with tags
Pricing pageThree tiers and an FAQ
Onboarding emailA five-step welcome series
Status pageUptime and incident history
Release notesHighlights, fixes, upgrades
Team wikiNested pages with search
<Carousel opts={{ align: "start" }}>
  <CarouselContent>
    <CarouselItem className="basis-1/2 sm:basis-1/3"></CarouselItem>
  </CarouselContent>
</Carousel>

Spacing

Slides are separated by padding on CarouselItem and a matching negative margin on CarouselContent (16px by default). To change the gap, change both: -ml-2 on the content and pl-2 on each item.

Vertical

orientation="vertical" scrolls on the y-axis. Give CarouselContent a fixed height; the buttons move above and below.

09:00
Design reviewStudio A
10:30
Tokens syncRemote
12:00
Lunch & learn: motionAtrium
14:00
Accessibility auditStudio B
16:30
Release planningRemote

With the API

setApi hands you the Embla instance. Use it to build a slide counter, pagination dots or thumbnails, and to react to select events.

Dashboard
Settings
Billing
Activity
Team
1 / 5
const [api, setApi] = React.useState<CarouselApi>()

React.useEffect(() => {
  if (!api) return
  const onSelect = () => setCurrent(api.selectedScrollSnap())
  api.on("select", onSelect)
  return () => {
    api.off("select", onSelect)
  }
}, [api])

<Carousel setApi={setApi}></Carousel>

Guidelines

When to use

  • To browse a set of peer items where seeing a few at a time is enough: screenshots, templates, product photos, testimonials.
  • On touch devices, where swiping through a set is natural.

When not to use

  • For content everyone must see. Slides after the first are rarely viewed — put important information on the page.
  • For navigation or comparison — use Tabs, a grid, or a Table.
  • For a single hero image — just show the image.

Show that there's more

Let the next slide peek in, show a count or dots, and keep the previous button disabled on the first slide. People should never wonder whether there is more.

1 / 4

Do.Controls and a position indicator make the set's size obvious.
Next slide in 3s…
Don’t.Auto-advancing slides with no controls move content away before people finish reading.

Autoplay

prfct doesn't autoplay. If you add the Embla autoplay plugin, pause on hover and focus, provide a visible pause button, and don't autoplay at all when prefers-reduced-motion is set. Content that moves on its own for more than five seconds must be pausable (WCAG 2.2.2).

Accessibility

The carousel is a region with aria-roledescription="carousel", and every slide is a group with aria-roledescription="slide".

KeyBehavior
Tab
Moves focus into the slides and to the previous and next buttons.
Scrolls to the previous slide when focus is inside the carousel.
Scrolls to the next slide when focus is inside the carousel.
EnterSpace
Activates the focused previous or next button.
  • Name the carousel. Give Carousel an aria-label that says what the slides are: "Product screenshots".
  • Label slides by position. Give each CarouselItem an aria-label such as "2 of 5", so people know where they are.
  • Buttons are labeled. CarouselPrevious and CarouselNext include "Previous slide" and "Next slide" for screen readers — translated by LocaleProvider — and are disabled at the ends unless the carousel loops.
  • Text on images. Keep 4.5:1 contrast for text over imagery — use a scrim, as in the examples.

API reference

The root. Accepts all div props.

PropTypeDefault
opts

Embla options: align, loop, dragFree, slidesToScroll, breakpoints and more.

EmblaOptionsTypeNo default
plugins

Embla plugins, such as autoplay or wheel gestures.

EmblaPluginType[]No default
orientation

Scroll axis. Vertical carousels need a fixed content height.

"horizontal" | "vertical""horizontal"
setApi

Receives the Embla instance once it's ready.

(api: CarouselApi) => voidNo default

CarouselContent

The scrolling track. Accepts all div props; its negative margin pairs with the item padding to form the gap.

CarouselItem

A single slide, full width by default. Set basis-* to show several at once.

CarouselPrevious, CarouselNext

Outline icon buttons wired to the carousel. They accept all Button props; variant defaults to "outline" and size to "icon-sm".

useCarousel

Returns { api, scrollPrev, scrollNext, canScrollPrev, canScrollNext, orientation } for building custom controls inside a Carousel.