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.
Drivn and shadcn/ui both build their Carousel on top of embla-carousel-react, the same lightweight slider engine that owns the touch drag, snap, momentum, and viewport math. That means the runtime cost is identical — Embla does the heavy lifting on both sides, and any plugin written against Embla works in either library. Where the two diverge is the surface each one exposes around Embla and which composition pieces ship in the box.
shadcn's Carousel exports four named components — CarouselContent, CarouselItem, CarouselPrevious, CarouselNext — and stops there. If you want pagination dots, you implement them yourself by reading scrollSnapList() and selectedScrollSnap() off the Embla API and wiring the click handlers manually. Drivn collapses the API into dot notation — Carousel.Content, Carousel.Item, Carousel.Previous, Carousel.Next — and ships Carousel.Dots as a built-in subcomponent that already wires the snap list, the active index, and the click-to-scroll callback.
This page walks through every surface that differs: API shape, the Dots component, the keyboard nav baseline, orientation handling, and external API access via setApi. Every snippet below compiles against the latest Carousel source written by the Drivn CLI. If you have shadcn's Carousel today, the migration is a few imports and deleting your hand-rolled dots component.
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
Pagination dots are the most common Carousel addition after the prev/next arrows, and they are the surface where Drivn and shadcn diverge most. shadcn does not ship a Dots component — you implement it yourself by reading api.scrollSnapList() for the snap count, tracking api.selectedScrollSnap() on the select event, and wiring api.scrollTo(index) per dot. That is ten to fifteen lines of boilerplate plus a useState and a useEffect you copy into every project.
Drivn ships Carousel.Dots with all of that pre-wired. The component reads the snap list from context, marks the active dot via selectedIndex === i, and calls api.scrollTo(i) on click. You drop one tag inside the carousel and it renders a row of clickable dots underneath the slides. The styling tokens — bg-border for inactive, bg-foreground for active — sit in the styles.dots object inside the Carousel source for easy theming.
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.