Context
I need a carousel component to showcase blog posts on the landing page with auto-scrolling and responsive behavior (1-3 posts per view across mobile to desktop).
Traditional carousel solutions present several tradeoffs:
- Swiper.js: Feature-rich and popular, but heavy (30KB+ gzipped) with many unused features for this simple use case
- React Slick: Widespread in the React ecosystem, but built on jQuery dependencies and lacks TypeScript support
- Keen Slider: Lightweight and TypeScript-first, but less mature ecosystem and fewer plugins
- Pure CSS + Scroll Snap: Minimal overhead, but lacks programmatic control for auto-scroll and complex navigation patterns
- Building Custom: Maximum control, but introduces maintenance burden for solving an already-solved problem
I want a solution that prioritizes:
- Headless Architecture: Pure logic with no UI opinions, allowing me to standardize the design language across the site using my own components and Tailwind classes
- Performance: Lightweight bundle size and smooth 60fps animations
- TypeScript Support: First-class types that align with the LLM-Optimized principle
- Plugin Architecture: Extensibility without bloat—load only what's needed
- Framework Flexibility: React integration today, but not locked into React if requirements change
- AI Familiarity: Well-documented APIs that LLMs can work with effectively
Decision
I decided to use Embla Carousel with the following packages:
embla-carousel: Framework-agnostic core (~6KB gzipped)embla-carousel-react: React wrapper with hooksembla-carousel-auto-scroll: Plugin for auto-scrolling behavior
This aligns with The Goldilocks Zone and LLM-Optimized. Embla is TypeScript-first, has clear documentation, and uses a composable hook-based API that AI agents understand well.
Notably, Embla provides a builder/playground on their website where you can generate and copy navigation code (buttons, dots, auto-scroll) directly into your codebase—similar to the shadcn/ui philosophy (ADR 019). This gives us ownership of the implementation rather than hiding it behind opaque library abstractions.
Interactive Demo
Here's the carousel in action, using the same ContentCarousel component that powers the blog and ADR carousels across this site:
Consequences
Pros
- Truly Headless: Embla provides only carousel logic—no pre-styled UI components or CSS to override. This allows complete control over the design language, ensuring carousel navigation matches the rest of the site's Tailwind-based aesthetic without fighting vendor styles.
- Lightweight: The core is ~6KB gzipped—significantly smaller than Swiper (~31-45KB). We only load the auto-scroll plugin (~2KB) when needed.
- Design System Consistency: Because we build navigation components from scratch, they naturally align with our existing UI patterns (shadcn/ui components, Tailwind design tokens). No visual inconsistencies from library-imposed styles.
- TypeScript-First: Excellent type inference with
EmblaCarouselTypeandEmblaOptionsType. The API is fully typed, reducing runtime errors and improving the agentic workflow. - Performance: Hardware-accelerated transforms, smooth 60fps animations, and no jQuery overhead.
- Composability: Plugin architecture means we pay only for features we use. The
AutoScrollplugin handles the auto-scrolling behavior without polluting the core API. - Code Ownership: The builder provides copy-paste navigation hooks (
usePrevNextButtons,useDotButton) that live in our codebase (ui/components/ui/carousel.tsx). This is transparent, modifiable, and easier for AI agents to reason about than importing opaque components fromnode_modules. - React Hooks Integration: The
useEmblaCarouselhook fits naturally into React patterns. Custom hooks encapsulate carousel state logic cleanly. - Framework-Agnostic Core: If we migrate away from React in the future, the core carousel logic remains reusable.
Cons
- Initial Setup Overhead: Requires copying navigation code from the builder rather than importing pre-built components. However, this is a one-time cost and aligns with our shadcn philosophy of code ownership.
- Smaller Ecosystem: Fewer third-party plugins compared to Swiper, though the core plugin set (auto-scroll, auto-play, class names) covers most use cases.