Drivn vs shadcn/ui — Carousel Component Compared
Drivn vs shadcn/ui React Carousel: both run on embla-carousel-react. Drivn ships dot-notation API and built-in Carousel.Dots — shadcn does not.
Both Drivn and shadcn/ui hand the hard part of a carousel to the same package: embla-carousel-react. Drag physics, snap points, momentum, resize recalculation — Embla owns all of it in either library, so bundle weight and scroll feel come out a wash. The question worth asking is not which engine performs better. It is how much of the surrounding component you still have to write yourself.
There the answers split. shadcn exposes four named exports — CarouselContent, CarouselItem, CarouselPrevious, CarouselNext — and leaves pagination to you: read scrollSnapList() for the count, subscribe to Embla's select event for the active index, call scrollTo(i) from each dot. Drivn hangs the same primitives off one root via Object.assign, so the entire API is Carousel.Content, Carousel.Item, Carousel.Previous, Carousel.Next — plus Carousel.Dots, which pulls scrollSnaps and selectedIndex out of context and takes no props at all.
The sections below take each divergence in turn: import shape, the Dots subcomponent, what the root wires up for keyboard and ARIA, orientation, and the setApi escape hatch for driving the deck from a parent. Every snippet is checked against the Carousel source the Drivn CLI writes into your repo. Coming from shadcn, the port is a swap of import lines plus deleting the dots component you hand-rolled.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying library | embla-carousel-react | embla-carousel-react |
| API style | Dot notation (Carousel.Content) | Named exports (CarouselContent) |
| Pagination dots | Carousel.Dots built-in | Hand-roll with scrollSnapList() |
| Keyboard arrow nav | Wired on root | Wired on root |
| Orientation | horizontal | vertical | horizontal | vertical |
| External API access | setApi callback | setApi callback |
| Plugin support | plugins prop forwards | plugins prop forwards |
| Runtime UI deps | embla + lucide-react | embla + lucide-react |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
shadcn's Carousel exports four separate components and you import each one explicitly. Drivn keeps the same primitives but mounts them on the Carousel root via Object.assign, so a single import line covers every piece you need — Carousel.Content, Carousel.Item, Carousel.Previous, Carousel.Next, Carousel.Dots. The Carousel docs cover every prop forwarded to Embla.
The import diff is small per file but it compounds. A page with two carousels saves four imports in Drivn, and the dot-notation lookup gives autocomplete the right shape without you remembering whether the export is named CarouselNext or CarouselNextButton. TypeScript narrows each subcomponent's props from the same root type, so renaming the root renames every child reference at once.
1 // shadcn/ui — four named exports per file 2 import { 3 Carousel, 4 CarouselContent, 5 CarouselItem, 6 CarouselPrevious, 7 CarouselNext, 8 } from '@/components/ui/carousel' 9 10 <Carousel> 11 <CarouselContent> 12 <CarouselItem>Slide 1</CarouselItem> 13 <CarouselItem>Slide 2</CarouselItem> 14 </CarouselContent> 15 <CarouselPrevious /> 16 <CarouselNext /> 17 </Carousel> 18 19 // Drivn — one import, dot notation 20 import { Carousel } from '@/components/ui/carousel' 21 22 <Carousel> 23 <Carousel.Content> 24 <Carousel.Item>Slide 1</Carousel.Item> 25 <Carousel.Item>Slide 2</Carousel.Item> 26 </Carousel.Content> 27 <Carousel.Previous /> 28 <Carousel.Next /> 29 </Carousel>
Built-in pagination dots
Dots are the first thing most teams add after the arrows, and they are the cleanest split between the two libraries. shadcn ships no Dots component, so the work lands in your app code: hold the Embla instance with setApi, pull the snap count from api.scrollSnapList(), track the active index by subscribing to select, and call api.scrollTo(i) from each button. That is a pair of useState hooks, a useEffect, and a map — rewritten in every project that needs a slider.
Drivn moves that bookkeeping into the root. CarouselRoot already holds scrollSnaps and selectedIndex in its context and refreshes both on Embla's reInit and select events, which leaves Carousel.Dots as a stateless consumer: it maps over scrollSnaps, applies styles.dots.active when i === selectedIndex, and renders each dot as a <button type="button"> labelled Go to slide {i + 1}. Because the snap list is rebuilt on reInit, adding or removing slides updates the dot count on its own. Theming lives in the styles.dots object in the Carousel source — bg-border inactive, bg-foreground active.
1 // shadcn/ui — hand-roll pagination dots 2 'use client' 3 import { useState, useEffect } from 'react' 4 import { Carousel, type CarouselApi } from '@/components/ui/carousel' 5 6 export function Slider() { 7 const [api, setApi] = useState<CarouselApi>() 8 const [current, setCurrent] = useState(0) 9 const [count, setCount] = useState(0) 10 11 useEffect(() => { 12 if (!api) return 13 setCount(api.scrollSnapList().length) 14 setCurrent(api.selectedScrollSnap()) 15 api.on('select', () => setCurrent(api.selectedScrollSnap())) 16 }, [api]) 17 18 return ( 19 <Carousel setApi={setApi}> 20 {/* slides... */} 21 <div className="flex gap-2 mt-3"> 22 {Array.from({ length: count }).map((_, i) => ( 23 <button 24 key={i} 25 onClick={() => api?.scrollTo(i)} 26 className={i === current ? 'bg-foreground' : 'bg-border'} 27 /> 28 ))} 29 </div> 30 </Carousel> 31 ) 32 } 33 34 // Drivn — one tag, done 35 import { Carousel } from '@/components/ui/carousel' 36 37 <Carousel> 38 <Carousel.Content> 39 <Carousel.Item>Slide 1</Carousel.Item> 40 <Carousel.Item>Slide 2</Carousel.Item> 41 <Carousel.Item>Slide 3</Carousel.Item> 42 </Carousel.Content> 43 <Carousel.Dots /> 44 </Carousel>
Orientation and external API
Both libraries accept an orientation prop set to horizontal or vertical and forward it to Embla as the axis option. Both also pass a setApi callback so the parent component can grab the Embla API and drive the carousel from outside — useful for syncing two carousels, building a "go to slide N" button outside the component, or pausing autoplay on hover.
The shape of setApi is identical between Drivn and shadcn — both emit a CarouselApi type which is the second tuple element from useEmblaCarousel. If you already use shadcn's Carousel and have code that calls api.scrollTo(2) from a parent, the same code works in Drivn after you swap the imports. For the full Embla API surface, see the embla-carousel-react documentation.
1 'use client' 2 import { useState, useEffect } from 'react' 3 import { Carousel, type CarouselApi } from '@/components/ui/carousel' 4 5 export function VerticalSlider() { 6 const [api, setApi] = useState<CarouselApi>() 7 8 useEffect(() => { 9 api?.scrollTo(0) 10 }, [api]) 11 12 return ( 13 <Carousel orientation="vertical" setApi={setApi}> 14 <Carousel.Content className="h-[300px]"> 15 <Carousel.Item>Slide 1</Carousel.Item> 16 <Carousel.Item>Slide 2</Carousel.Item> 17 <Carousel.Item>Slide 3</Carousel.Item> 18 </Carousel.Content> 19 </Carousel> 20 ) 21 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Just embla-carousel-react for the slider engine and lucide-react for the chevron icons on Carousel.Previous and Carousel.Next. Both are dependencies you almost certainly already have if you use any modern React component library. There is no Radix, no cva, no clsx wrapper, no floating-ui — Embla owns the slide logic and Tailwind owns the styling.
Because pagination dots are the single most common Carousel addition and the boilerplate to build them — wiring scrollSnapList(), listening to select, calling scrollTo() per dot — is identical in every implementation. Shipping it as a subcomponent removes ten lines of glue per slider. The dots also auto-update when the slide count changes, which a hand-rolled version often misses.
Yes. Pass any Embla plugin array via the plugins prop and Drivn forwards it to useEmblaCarousel unchanged. Autoplay, WheelGestures, ClassNames, AutoScroll — all the official Embla plugins work without modification. The plugin contract is owned by Embla, so anything documented in the Embla plugins docs applies.
The root has role="region" and aria-roledescription="carousel", and each item has aria-roledescription="slide" — that is the WAI-ARIA pattern for carousels. Arrow key navigation works by default. For active-slide announcements on every change, add a polite ARIA live region tied to the select event from setApi. Drivn does not ship that wiring because Embla emits select on every drag pixel and a naive announcement creates a screen-reader storm.

