Skip to content
Drivn
5 min read

React Carousel Component Examples

Drop-in React Carousel examples: basic slider, arrows, built-in pagination dots, loop, multiple slides per view, and vertical orientation. Built on Embla.

A carousel is one of the few UI patterns where writing it from scratch is a bad trade. Momentum dragging, snap points, pointer capture, and the resize math behind them add up to a lot of code to own for a testimonial slider — which is why most React carousels in production are a wrapper around embla-carousel-react. Drivn's Carousel is that wrapper, kept deliberately thin: Embla owns motion, Drivn owns layout, arrows, and dots, and the whole surface hangs off one dot-notation import — Carousel.Content, Carousel.Item, Carousel.Previous, Carousel.Next, Carousel.Dots.

Every snippet below is copy-paste ready against the source the Drivn CLI writes into your repo. Out of the box you get a horizontal track, one full-width slide per view (min-w-0 shrink-0 grow-0 basis-full), arrows that disable themselves at each end, and Left/Right key handling registered on the root through a handleKeyDown callback — no effect to wire, no ref to thread.

Everything past that is a prop. orientation="vertical" flips Embla's axis to y. opts forwards straight into useEmblaCarousel, so loop, align, and the rest of EmblaOptionsType behave exactly as the Embla docs describe. plugins takes the autoplay package or any other Embla plugin unchanged, and setApi hands the live CarouselApi back to a parent that needs to drive the deck itself.

Basic slider with navigation arrows

The minimum carousel is a root with a Carousel.Content and a list of Carousel.Item children. Each item is min-w-0 shrink-0 grow-0 basis-full by default, so one slide fills the viewport at a time. Add Carousel.Previous and Carousel.Next as siblings inside the root and you get a slider with prev/next chevron buttons positioned outside the viewport at -left-12 and -right-12.

The arrow buttons disable themselves automatically when there is no previous or next slide — the Carousel source tracks canScrollPrev and canScrollNext from the Embla API and passes them as the disabled prop on the underlying Button. Keyboard arrow keys work without any extra wiring: focus the carousel, press Left or Right, and Embla scrolls.

1import { Carousel } from "@/components/ui/carousel"
2
3export default function Page() {
4 return (
5 <Carousel>
6 <Carousel.Content>
7 <Carousel.Item>Slide 1</Carousel.Item>
8 <Carousel.Item>Slide 2</Carousel.Item>
9 <Carousel.Item>Slide 3</Carousel.Item>
10 </Carousel.Content>
11 <Carousel.Previous />
12 <Carousel.Next />
13 </Carousel>
14 )
15}

Pagination dots

Carousel.Dots reads everything it needs from context, so adding pagination is one self-closing tag inside the root. The component maps over scrollSnaps — the array the root captures from api.scrollSnapList() once Embla mounts — renders a <button type="button"> per snap point, and marks the current one by comparing i === selectedIndex, which stays in sync through Embla's select and reInit events. Clicking a dot calls api?.scrollTo(i). None of that plumbing reaches your page: no useState, no effect, no snap bookkeeping.

The styling stays deliberately small — flex justify-center gap-1.5 mt-3 on the wrapper, w-2 h-2 rounded-full bg-border per dot, bg-foreground on the active one — and every button carries an aria-label of the form "Go to slide 3", so the control is usable without sight of the row. Dots and arrows are independent. Render both once the deck runs past three or four slides and the reader benefits from position feedback plus stepping; keep dots alone for a touch-first mobile gallery where the Button-based arrows would crowd the frame. For a single hero image, drop both and let drag carry it.

1<Carousel>
2 <Carousel.Content>
3 <Carousel.Item>Slide 1</Carousel.Item>
4 <Carousel.Item>Slide 2</Carousel.Item>
5 <Carousel.Item>Slide 3</Carousel.Item>
6 </Carousel.Content>
7 <Carousel.Dots />
8</Carousel>

Infinite loop

Pass opts={{ loop: true }} on the Carousel root and Embla wraps around — scrolling past the last slide returns to the first, and canScrollPrev stays true at the start. Loop mode is the right default for testimonial sliders, autoplay banners, and any carousel where the slide order is not chronological. For step-by-step onboarding flows, leave loop off so the prev arrow disables on the first slide and the next arrow disables on the last.

The opts prop accepts every option from EmblaOptionsType — alignment (align: 'start' | 'center' | 'end'), drag-free mode, slide-skip behavior, and so on. The full option list lives in the Embla docs; Drivn forwards the object verbatim to useEmblaCarousel without any transformation.

1<Carousel opts={{ loop: true }}>
2 <Carousel.Content>
3 <Carousel.Item>Slide 1</Carousel.Item>
4 <Carousel.Item>Slide 2</Carousel.Item>
5 <Carousel.Item>Slide 3</Carousel.Item>
6 </Carousel.Content>
7 <Carousel.Previous />
8 <Carousel.Next />
9</Carousel>

Multiple slides per view

By default each Carousel.Item has basis-full, so one slide fills the viewport. Override the basis class on each item — basis-1/3 for three per view, basis-1/2 for two, basis-1/4 for four — and the carousel renders multiple slides at once with the rest queued for scroll. Pair it with opts={{ align: 'start' }} so the leftmost visible slide aligns to the left edge of the viewport rather than centering.

This is the right pattern for product card rows, image grids that scroll horizontally, or category tile carousels. The combination of basis-1/3 and align: 'start' is the most common — three cards visible, the user can drag or click to reveal the next set. For responsive layouts, swap basis classes via Tailwind breakpoints: basis-full md:basis-1/2 lg:basis-1/3.

1<Carousel opts={{ align: "start" }}>
2 <Carousel.Content>
3 <Carousel.Item className="basis-1/3">
4 Slide 1
5 </Carousel.Item>
6 <Carousel.Item className="basis-1/3">
7 Slide 2
8 </Carousel.Item>
9 <Carousel.Item className="basis-1/3">
10 Slide 3
11 </Carousel.Item>
12 <Carousel.Item className="basis-1/3">
13 Slide 4
14 </Carousel.Item>
15 <Carousel.Item className="basis-1/3">
16 Slide 5
17 </Carousel.Item>
18 </Carousel.Content>
19 <Carousel.Previous />
20 <Carousel.Next />
21</Carousel>

Vertical orientation

Set orientation="vertical" on the root and the carousel scrolls top-to-bottom instead of left-to-right. The Carousel source flips the Embla axis to y, swaps the container layout to flex-col, and changes the item spacing class from pl-4 to pt-4. The Carousel.Content needs an explicit height — h-[300px] works for most cases — because vertical scroll needs a bounded container.

Vertical carousels are less common but show up in mobile-first feeds, sidebar navigation, and timeline-style content. The arrow buttons still work but their default position is tuned for horizontal layouts; override className on Carousel.Previous and Carousel.Next if you want them above and below the slides instead of left and right.

1<Carousel orientation="vertical">
2 <Carousel.Content className="h-[300px]">
3 <Carousel.Item>Slide 1</Carousel.Item>
4 <Carousel.Item>Slide 2</Carousel.Item>
5 <Carousel.Item>Slide 3</Carousel.Item>
6 </Carousel.Content>
7</Carousel>
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

No. The Carousel source imports useEmblaCarousel from embla-carousel-react and two chevron icons from lucide-react. The prev/next buttons reuse the existing Button component. No Radix, no cva, no floating-ui — the slider is Embla, the arrows are styled Button instances, and the layout is Tailwind.

Yes. Pass a setApi callback prop and Drivn calls it with the Embla API once the carousel mounts. Store the API in useState<CarouselApi>() and you can call api.scrollTo(2), api.scrollNext(), or any other Embla method from a parent button, a sibling component, or an effect that responds to URL changes.

Install embla-carousel-autoplay and pass it via the plugins prop on the Carousel root. The plugin accepts options like delay, stopOnInteraction, and stopOnMouseEnter. The contract is owned by Embla so the same plugin works in shadcn, Drivn, or any other Embla wrapper without modification — Drivn just forwards the array.

Because the most common slider is one slide per view, and basis-full paired with min-w-0 shrink-0 grow-0 makes each slide fill the viewport regardless of content size. Override basis-full with basis-1/3 or any other Tailwind basis class on individual items to render multiple slides per view. The default exists so the simplest case requires zero className overrides.

The Carousel file starts with "use client" because Embla uses useState, useEffect, and event handlers. You can import it from a Server Component, but it renders on the client. If your page is a Server Component, leave the import where it is — Next.js draws the client boundary correctly based on the use client directive at the top of the file.