# Carousel

> A swipeable React carousel on Embla for photos, cards, testimonials and onboarding, with peek, arrows, dots, thumbnails and autoplay.

- Type: React component (Media)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.1.0
- Install: `npx wingo-ui@latest add carousel`
- Docs: https://wingo-ui.com/components/carousel

## Interaction

Drag or swipe the slides: embla's physics carry them and they snap to the nearest slide (dragFree lets a rail glide without snapping). On a trackpad, a two-finger swipe sideways moves one slide per swipe (Shift + mouse wheel too), and the page does not go back or forward. With a mouse, round arrows sit on the photo (or beside it, or in the header); they fade out at the ends and hide when there is nothing to scroll. Click a dot, a story bar or a thumbnail to jump there; the active dot stretches on a spring. With focus anywhere inside (the viewport itself is focusable), Left and Right (Up and Down when vertical) move one slide, Home and End jump to the first and last; inputs inside keep their own arrow keys. Dots and thumbnails are one Tab stop each (focus follows the selection), slides out of view are inert so Tab never lands on a hidden link (except in dragFree rails, which scroll a focused slide into view), and an arrow that turns disabled at the end hands its focus to the other one. Autoplay fills the bars as it runs and pauses while hovered, while keyboard focus is inside, while dragging, when the tab is hidden and when the carousel is off screen; the pause / play button is always there. On a phone arrows hide, 12% of the next slide peeks, dots get 44px tap areas and vertical page scrolling is never blocked. Under reduced motion arrows jump, effects are off and autoplay starts paused.

## Installation

```bash
npx wingo-ui@latest add carousel
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/carousel` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/carousel.tsx`

npm dependencies: `npm i embla-carousel-react motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Carousel } from "@/components/ui/carousel"

const photos = ["/demo/apartment.jpg", "/demo/sofa.jpg", "/demo/city.jpg"]

export function ListingPhotos() {
  return (
    <Carousel label="Apartment photos" indicator="counter" indicatorPosition="overlay" thumbnails={photos}>
      {photos.map((src, i) => (
        <img key={src} src={src} alt={`Photo ${i + 1}`} draggable={false} className="aspect-[4/3] w-full object-cover" />
      ))}
    </Carousel>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` |  | The slides. Each child is wrapped in a CarouselSlide (role group, "3 of 8") unless it already is one. Images inside should be draggable={false}. |
| `slidesPerView?` | `number \| "auto" \| { base?: number; sm?: number; md?: number; lg?: number }` | `"1"` | Slides visible at once. Fractions peek (1.15 shows 15% of the next slide). An object sets it by the carousel's own width (container queries: base, sm from 640px, md from 768px, lg from 1024px), so a rail in a narrow column shows fewer slides and the server layout is already right. "auto" sizes slides by their content width. The playground offers 1, 1.15, 2, 3 and auto. |
| `peek?` | `boolean` | `true` | While the carousel is narrower than 640px (a phone), whole-number slidesPerView values show 12% of the next slide (1 becomes 1.12), so it always hints that there is more. |
| `gap?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Space between slides: 0 / 8 / 12 / 16px. |
| `align?` | `"start" \| "center"` | `"start"` | Where the selected slide sits in the viewport. center suits the scale effect and testimonials. |
| `orientation?` | `"horizontal" \| "vertical"` | `"horizontal"` | vertical scrolls up and down (tickers, stories); give the viewport a height through classNames.viewport (320px by default). |
| `loop?` | `boolean` | `false` | Wraps from the last slide to the first. Embla turns it off by itself when there are too few slides to fill the view. |
| `dragFree?` | `boolean` | `false` | Momentum without snapping, for rails of chips or cards. |
| `wheel?` | `boolean` | `true` | A two-finger trackpad swipe (or Shift + mouse wheel) along the track moves it one slide per swipe; dragFree tracks follow the swipe. |
| `arrows?` | `"auto" \| "always" \| "never"` | `"auto"` | auto shows the arrows on fine pointers only (phones swipe) and hides them when there is nothing to scroll; always shows them on touch too. |
| `arrowPosition?` | `"inside" \| "outside" \| "header"` | `"inside"` | inside: round glass buttons over the middle of the viewport. outside: beside the viewport from 1024px up (inside below). header: a pair at the top end, next to the title of a rail. |
| `indicator?` | `"dots" \| "bars" \| "progress" \| "counter" \| "none"` | `"dots"` | dots: 6px dots, the active one stretched to 18px. bars: story-like segments that autoplay fills. progress: one thin bar tracking the scroll. counter: a "3 / 8" pill. |
| `indicatorPosition?` | `"below" \| "overlay"` | `"below"` | overlay puts the indicator (and the pause button) inside the bottom of the viewport on a dark gradient, for photo galleries. |
| `effect?` | `"none" \| "scale" \| "parallax"` | `"none"` | scale: slides away from the snap shrink to 0.9 and dim. parallax: media marked data-carousel-parallax shifts against the drag. Both read --slide-progress, written per scroll event (never React state); off under reduced motion. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Arrow and indicator sizes: arrows 32 / 40 / 48px, dots 5 / 6 / 8px, thumbnails 48 / 56 / 64px. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Slide and viewport corners: 0 / 12 / 16 / 24px. |
| `autoplay?` | `boolean` | `false` | Advances every interval (our own timer, no plugin). Pauses while hovered, focused, dragged, hidden or off screen, stops at the end without loop, and always shows a pause / play button (WCAG 2.2.2). |
| `interval?` | `number` | `5000` | Milliseconds per slide while autoplaying. |
| `pauseOnHover?` | `boolean` | `true` | Autoplay waits while a mouse is over the carousel. |
| `stopOnInteraction?` | `boolean` | `false` | false: a drag, an arrow or a dot restarts the timer; true: autoplay stops until the play button is pressed. |
| `label?` | `string` | `"Carousel"` | The region's aria-label, read as "Carousel, carousel" by screen readers. Name it after its content. |
| `title?` | `ReactNode` |  | A heading above the viewport; the header arrows sit beside it. It also names the region (aria-labelledby) unless label is set. |
| `thumbnails?` | `(string \| ReactNode)[]` |  | A synced strip under the viewport: image urls or nodes, one per slide. The selected one stays in view. |
| `index?` | `number` |  | The selected snap, controlled. Pair with onIndexChange. |
| `defaultIndex?` | `number` | `0` | The snap shown first, uncontrolled. The playground remounts the carousel when it changes. |
| `onIndexChange?` | `(index: number) => void` |  | Fires when the selected snap changes, by drag, arrow, dot, key or autoplay. |
| `labels?` | `Partial<CarouselLabels>` |  | Every string: previous, next, slide(i, n) ("3 of 8"), goTo(i) ("Go to slide 3"), pause, play, carousel and slideRole (the aria-roledescriptions). English by default, DEFAULT_CAROUSEL_LABELS. |
| `opts?` | `EmblaOptionsType` |  | Anything else for embla (slidesToScroll, skipSnaps, duration, containScroll...). Wins over the props above. |
| `setApi?` | `(api: CarouselApi) => void` |  | Receives the embla api once it is ready. |
| `classNames?` | `Partial<Record<"header" \| "title" \| "stage" \| "viewport" \| "container" \| "slide" \| "previous" \| "next" \| "indicator" \| "dot" \| "counter" \| "autoplay" \| "thumbnails" \| "thumbnail" \| "footer" \| "overlay", string>>` |  | Extra classes for the inner parts. |

## Parts

### CarouselRoot (component)

The compound root: a <section aria-roledescription="carousel"> that owns embla, autoplay and the keyboard. Takes every behavior prop above (not the arrow / indicator layout ones), plus scrim (a dark gradient at the bottom of each slide for white controls overlaid on photos). Props: CarouselRootProps.

### CarouselViewport (component)

The embla viewport and its container; wraps children in slides and numbers them. Props: ComponentProps<"div"> & { classNames?: { container?, slide? } }.

### CarouselSlide (component)

One slide: role group, data-active, inert while out of view, --slide-progress for effects. Props: ComponentProps<"div"> & { index?: number; total?: number }.

### CarouselPrevious (component)

The previous arrow, disabled at the start (inside arrows fade out there). Props: ButtonProps & { position?: "inside" | "outside" | "header"; visibility?: "auto" | "always" }.

### CarouselNext (component)

The next arrow, disabled at the end. Props: ButtonProps & { position?: "inside" | "outside" | "header"; visibility?: "auto" | "always" }.

### CarouselDots (component)

Dots, story bars or a progress bar; overlay is white-based for photos. Props: ComponentProps<"div"> & { variant?: "dots" | "bars" | "progress"; overlay?: boolean }.

### CarouselCounter (component)

The "3 / 8" pill. Props: ComponentProps<"span"> & { overlay?: boolean }.

### CarouselAutoplayToggle (component)

The pause / play button; renders only while autoplay is on. Props: ButtonProps & { overlay?: boolean }.

### CarouselThumbnails (component)

A second, drag-free embla strip synced to the selection. Props: ComponentProps<"div"> & { items: (string | ReactNode)[] }.

### useCarousel (hook)

State and controls of the nearest carousel, for custom controls (an onboarding Continue button). progress is the autoplay motion value, 0 to 1. Signature: () => { api, selectedIndex, snapCount, canScrollPrev, canScrollNext, scrollTo, scrollPrev, scrollNext, playing, setPlaying, progress }.

### DEFAULT_CAROUSEL_LABELS (constant)

The English strings for the arrows, slide names ("3 of 8"), dot names ("Go to slide 3"), autoplay pause and play, and the aria role descriptions. Spread it to translate.

### CarouselApi (type)

The Embla API instance, for scrollTo, events and plugins when you need more than the props.

## Examples

- **Listing rail**: Cards at 1.15 per view on phones, 2.5 and 3.5 wider; arrows in the header next to the title.
- **Autoplay hero**: Loops every 4.5s; the story bars fill with the timer. Hover, focus or drag to pause; the button stops it.
- **Gallery with thumbnails**: A synced strip under the photo; the counter sits on the image.
- **Testimonials with scale**: Centered and looping; off-center slides shrink and dim as they move away.
- **Onboarding**: Compound parts with useCarousel: Continue calls scrollNext, Get started on the last step.
- **Chip rail**: dragFree with auto-sized slides: momentum without snapping.
- **Vertical ticker**: Compound parts: a vertical loop every 3s, with the pause button in the header.

## See also

- [Card](https://wingo-ui.com/components/card) ([Markdown](https://wingo-ui.com/components/card.md))
- [Scroll Area](https://wingo-ui.com/components/scroll-area) ([Markdown](https://wingo-ui.com/components/scroll-area.md))
- [Step Player](https://wingo-ui.com/components/step-player) ([Markdown](https://wingo-ui.com/components/step-player.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))

---

Canonical: https://wingo-ui.com/components/carousel
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
