Skip to content
Drivn
5 min read

How to Add a Carousel to a React App

Step-by-step guide to adding a copy-and-own Carousel to any React project with the Drivn CLI — swipe, arrows, dots, loop, and multiple slides per view.

A carousel is one of those components everyone needs and nobody enjoys building from scratch — touch and swipe handling, snap points, looping, keyboard support, and dot indicators all have to agree with each other. The Drivn Carousel skips the from-scratch problem by wrapping embla-carousel-react, the proven headless slider engine, in a small, fully typed dot-notation surface you own outright.

Instead of installing a black-box carousel package, the CLI writes a carousel.tsx file into your repository. Inside, CarouselRoot calls useEmblaCarousel, tracks canScrollPrev, canScrollNext, and the selected index in local state, and shares them through a context so Carousel.Previous, Carousel.Next, and Carousel.Dots stay in sync. Arrow-key navigation is wired on the root via an onKeyDown handler, and the arrows are built on Drivn's own Button component. Because it relies on Embla's hook, the file is a client component marked 'use client' — but the page around it can still render on the server.

This guide adds the Carousel to an existing React app in about ten minutes: install the CLI, render slides with Carousel.Content and Carousel.Item, add arrows and dots, then turn on looping, multiple slides per view, and vertical orientation through Embla options. It works the same in Vite + React or a Next.js App Router app.

Prerequisites

Before installing the Carousel, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Carousel pulls in embla-carousel-react for the slider engine and lucide-react for the chevron icons, and it depends on the Drivn Button for its arrow controls — the CLI adds the npm package and writes the Button source for you during install if either is missing.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Carousel source. The command prompts once for your install directory (defaulting to src/components/ui/), writes carousel.tsx, adds embla-carousel-react to your package.json if it is missing, and — because the arrows are built on it — also writes button.tsx alongside. No global config file is created; the Carousel is a TypeScript file in your repo that you edit like any other component. Confirm both files landed in your editor, then commit them. If your project uses a monorepo layout or a non-standard path, the CLI docs cover the flags for targeting a custom location during install.

1# add the Carousel to your existing React project
2npx drivn add carousel
3
4# verify the files were written
5ls src/components/ui/carousel.tsx src/components/ui/button.tsx

Step 2 — Render slides with Content and Item

The Carousel manages its own scroll state, so you do not wire up useState — you just declare the slides. Import the component and nest Carousel.Item children inside Carousel.Content; the root sets up the Embla viewport and the items become the snap points. Each Carousel.Item is min-w-0 shrink-0 grow-0 basis-full by default, so one slide fills the frame and the rest sit off-screen until you scroll. The source begins with 'use client' because it calls useEmblaCarousel, so in a Next.js App Router project keep the Carousel in a client component and render it as an island. Touch and trackpad swipe work immediately — Embla handles the drag — and the root is focusable with arrow-key support already wired. See the Carousel docs for the full API.

1'use client'
2import { Carousel } from '@/components/ui/carousel'
3
4export function Gallery() {
5 return (
6 <Carousel>
7 <Carousel.Content>
8 <Carousel.Item>Slide 1</Carousel.Item>
9 <Carousel.Item>Slide 2</Carousel.Item>
10 <Carousel.Item>Slide 3</Carousel.Item>
11 </Carousel.Content>
12 </Carousel>
13 )
14}

Step 3 — Add navigation arrows and dots

Drop Carousel.Previous and Carousel.Next inside the Carousel to render the arrow controls, and Carousel.Dots for the indicator row. All three read the shared context, so they need no props — Previous and Next are Drivn Buttons that disable themselves when canScrollPrev or canScrollNext is false, and each renders a ChevronLeft or ChevronRight from lucide-react. Carousel.Dots maps Embla's scrollSnapList, renders one button per slide, marks the active one with the bg-foreground class, and calls scrollTo on click. The arrows are absolutely positioned just outside the frame, so leave horizontal room in the layout. Every control is keyboard accessible and carries an aria-label. For finished patterns, browse the Carousel examples.

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.Previous />
8 <Carousel.Next />
9 <Carousel.Dots />
10</Carousel>

Step 4 — Loop, multiple slides, and orientation

Behavior is configured through the opts prop, which passes straight to Embla. Set opts={{ loop: true }} to make the carousel wrap from the last slide back to the first. To show several slides at once, set opts={{ align: "start" }} and give each Carousel.Item a fractional basis like className="basis-1/3", which overrides the default basis-full — useful for product rows and logo walls. For a vertical slider, pass orientation="vertical", which flips Embla's axis and switches the item spacing from -ml-4/pl-4 to -mt-4/pt-4. Because every option is a plain prop, you change behavior without touching the source. The full prop and options reference lives in the Carousel docs; for the head-to-head see Drivn vs shadcn/ui Carousel.

1<Carousel opts={{ align: "start" }}>
2 <Carousel.Content>
3 <Carousel.Item className="basis-1/3">Slide 1</Carousel.Item>
4 <Carousel.Item className="basis-1/3">Slide 2</Carousel.Item>
5 <Carousel.Item className="basis-1/3">Slide 3</Carousel.Item>
6 <Carousel.Item className="basis-1/3">Slide 4</Carousel.Item>
7 </Carousel.Content>
8 <Carousel.Previous />
9 <Carousel.Next />
10</Carousel>
Get started

Install Drivn in one command

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

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add carousel. The Carousel has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. Vite + React is the most common non-Next setup and works without extra configuration; the CLI installs embla-carousel-react and writes the Button component the arrows depend on.

The Carousel is a client component — it is marked 'use client' because it calls useEmblaCarousel, which manages scroll state and DOM measurement with hooks. Your surrounding page and layout still render on the server. The usual pattern is a small client component holding the Carousel, rendered as an island inside an otherwise server-rendered route. Next.js inserts the client boundary at the import automatically.

Pass opts={{ loop: true }} to the Carousel root. The opts object forwards directly to embla-carousel-react, so any Embla option works the same way. With looping on, scrolling past the last slide wraps to the first and the previous arrow never disables, since there is always a slide in both directions. Combine it with an autoplay plugin via the plugins prop if you want hands-off advancing.

Set opts={{ align: "start" }} on the root and give each Carousel.Item a fractional basis such as className="basis-1/3" for three across or basis-1/2 for two. The class overrides the default basis-full, and because items merge their className through cn, you can mix widths. This is the standard pattern for product carousels, testimonial rows, and logo walls where several items share the frame.