Overview
The Carousel (also called Slider, Slideshow, or Swiper) presents a series of content items — images, cards, testimonials, or product listings — in a horizontally scrollable container that users cycle through one or a few items at a time.
Carousels are among the most debated components in UI design. Research consistently shows that auto-playing carousels on hero sections have abysmally low engagement (often < 1% click-through on slides beyond the first). However, when used correctly — user-initiated, clearly navigable, and focused — carousels are effective for browsing collections: product galleries, image portfolios, or related content suggestions.
When to use a Carousel:
- Product image galleries (e-commerce detail pages)
- Testimonial or review sections where space is limited
- Related content or "you may also like" suggestions
- Portfolio or case study showcases
- Onboarding flows with sequential slides (mobile)
- Multi-image social media-style posts
When NOT to use a Carousel:
- For hero banner rotation — users rarely engage past the first slide. Use a static hero or A/B test your best single option instead.
- For critical content the user must see — if it matters, don't hide it behind a swipe.
- For navigation — use Tabs or a Navigation Bar
- When all items should be visible simultaneously — use a scrollable Grid or List
Carousel animations — the slide, fade, or scale transitions between items — should be smooth and purposeful. Use the Animation Tool to configure easing curves and durations. Slide transitions benefit from spring-based easing for a physical feel. Configure transitions to ensure motion respects user preferences.
Variants
Common Carousel Variants
| Variant | Description | Use Case |
|---|---|---|
| Single Slide | One item visible at a time. Full-width or container-width. | Hero images, testimonials, onboarding steps. |
| Multi-Slide | Multiple items visible, scrolling by one or a group. | Product listings, related content, card grids. |
| Peek / Overflow | Current slide is centered; adjacent slides peek from the edges. | Signals that more content exists, encourages swiping. |
| Fade | Items crossfade rather than slide. | Image galleries where spatial movement is unnecessary. |
| Coverflow | 3D perspective effect with the active slide centered and raised. | Media galleries, album art browsing. Heavily animated — use Animation Tool. |
| Thumbnail Navigation | A secondary strip of thumbnail images for direct slide selection. | Product photo galleries, real estate listings. |
| Vertical | Items scroll vertically instead of horizontally. | Story-style content (Instagram stories), full-screen mobile feeds. |
Navigation Variants
| Navigation | Description |
|---|---|
| Arrow Buttons | Previous/Next buttons on the sides of the carousel. Always include for keyboard/mouse users. |
| Dot Indicators | Row of dots below the carousel showing position and total count. Click to jump to a slide. |
| Thumbnail Strip | Clickable thumbnails for direct navigation to any slide. |
| Progress Bar | A thin bar showing progression through slides. Useful for auto-playing carousels. |
| Counter | Text showing "3 / 12" for current position. Preferred when there are many slides. |
| Swipe Only | No visible controls, touch/drag only. Avoid — lacks discoverability and accessibility. |
Autoplay Variants
- No autoplay (recommended): User controls all navigation. Best for engagement and accessibility.
- Autoplay with pause on hover/focus: Slides advance automatically but stop when the user interacts. Must provide pause/play button.
- Autoplay with progress indicator: A progress bar fills as the slide timer counts down, giving users a sense of timing.
Properties
Carousel Properties
| Property | Type | Default | Description |
|---|---|---|---|
items | ReactNode[] | — | Array of slide content |
slidesPerView | number | 'auto' | 1 | Number of visible slides |
spaceBetween | number | 0 | Gap (px) between slides |
loop | boolean | false | Infinite loop navigation |
autoplay | boolean | { delay: number; pauseOnHover: boolean } | false | Automatic slide advancement |
navigation | boolean | true | Show prev/next arrow buttons |
pagination | 'dots' | 'fraction' | 'progress' | false | 'dots' | Slide position indicator style |
effect | 'slide' | 'fade' | 'coverflow' | 'cube' | 'slide' | Transition effect between slides |
speed | number | 300 | Transition duration in ms. Preview with Transition Tool. |
draggable | boolean | true | Enable touch/mouse drag navigation |
breakpoints | Record<number, Partial<CarouselProps>> | — | Responsive overrides at viewport widths |
a11y | { prevSlideMessage: string; nextSlideMessage: string } | — | Accessible labels for navigation |
onSlideChange | (index: number) => void | — | Callback when active slide changes |
Important: If autoplay is enabled, you must provide a visible pause/play button. This is a WCAG SC 2.2.2 requirement, not a suggestion. Users with cognitive or motor impairments need the ability to stop automatic content changes.
Token Mappings
Design Token Mappings
| Token Category | Token Example | Carousel Usage |
|---|---|---|
| Color – Surface | --color-surface-default | Slide container background |
| Color – Navigation | --color-neutral-700 | Arrow button icon fill |
| Color – Navigation BG | --color-surface-elevated | Arrow button background (semi-transparent) |
| Color – Indicator Active | --color-primary-600 | Active dot/progress fill |
| Color – Indicator Inactive | --color-neutral-300 | Inactive dot fill |
| Spacing | --space-4 | Gap between slides (spaceBetween) |
| Border Radius | --radius-full | Dot indicators (circular) |
| Border Radius | --radius-lg | Slide content rounding (card-style) |
| Shadow | --shadow-md | Navigation buttons floating over content |
| Transition | --duration-normal (300ms), --ease-out | Slide transition. Configure with Transition Tool and Animation Tool. |
| Z-Index | --z-overlay (10) | Navigation buttons above slide content |
Dot indicators should be at least 8×8px (44×44px touch target including padding) per WCAG SC 2.5.5 (Target Size). Small dots are fine visually, but the clickable area must be large enough for touch interaction.
States
Carousel States
| State | Description | Visual Treatment |
|---|---|---|
| Idle | No interaction. Showing current slide(s). | Static display. Autoplay timer running if enabled. |
| Dragging | User is touch/mouse dragging between slides. | Slides follow the pointer with momentum. Snap to nearest slide on release. Disable autoplay during drag. |
| Transitioning | Animating between slides. | Slide/fade/scale animation in progress. Duration 200–500ms. Use Animation Tool for easing. |
| At Start | First slide is active (non-looping mode). | Previous button is disabled (aria-disabled="true") or hidden. Visual dimming. |
| At End | Last slide is active (non-looping mode). | Next button is disabled or hidden. |
| Autoplay Running | Auto-advancing with timer. | Progress indicator filling. Play/pause button shows pause icon. |
| Autoplay Paused | Auto-advance paused by user interaction. | Progress indicator paused. Play/pause button shows play icon. |
| Loading | Slide content (images) still loading. | Skeleton or blurred placeholder. loading="lazy" on off-screen slide images. |
Drag physics: On release, calculate the drag velocity. If the velocity exceeds a threshold (e.g., 0.5px/ms), fling to the next slide regardless of drag distance. If below the threshold, snap back or forward based on the 50% crossing point. This creates a natural, momentum-based feel.
Accessibility
Accessibility Requirements
Carousels are notoriously problematic for accessibility. Doing them right requires careful attention to keyboard navigation, screen reader announcements, and motion preferences.
Semantic Structure:
- The carousel container should use
role="region"witharia-roledescription="carousel"and anaria-label(e.g., "Product images"). - Each slide should use
role="group"witharia-roledescription="slide"andaria-label(e.g., "Slide 3 of 12"). - Navigation buttons need
aria-label="Previous slide"andaria-label="Next slide". - Dot indicators should be implemented as a
tablistwithtabroles, or as a group of radio buttons.
WCAG Compliance:
- SC 1.4.3 (Contrast – Minimum): Navigation arrows and dot indicators must meet 4.5:1 contrast when overlaid on slide content. Use a semi-transparent background behind arrows to guarantee contrast regardless of slide content. Verify with Contrast Checker.
- SC 1.4.11 (Non-text Contrast): Arrow icons and dot indicators are non-text UI elements requiring 3:1 contrast.
- SC 2.1.1 (Keyboard): Users must be able to navigate slides with keyboard. Left/Right arrows should move between slides when the carousel is focused. Tab should move between interactive elements within the current slide.
- SC 2.2.2 (Pause, Stop, Hide): If autoplay is enabled, provide a visible pause mechanism. The carousel must stop auto-advancing when any element within it receives keyboard focus.
- SC 2.4.7 (Focus Visible): Arrow buttons and dot indicators must show visible focus rings.
- SC 2.5.5 (Target Size): Dot indicators and arrow buttons must have at least 24×24px touch targets (44×44px recommended for Level AAA).
- SC 1.3.1 (Info and Relationships): The current slide position must be programmatically determinable — use
aria-current="true"on the active dot.
Motion Preferences:
When prefers-reduced-motion: reduce is active, disable all slide animations — switch to instant transitions (no slide or fade). Also disable autoplay entirely. Some users set reduced motion because animation triggers vestibular disorders; an auto-advancing carousel is the worst offender.
Screen Reader Announcements:
Use aria-live="polite" on a visually hidden element that announces slide changes: "Slide 3 of 12: [slide title]". Do not announce every autoplay transition — only user-initiated navigation.
Usage Guidelines
Usage Guidelines
Do:
- Default to user-controlled navigation. Let users swipe, click arrows, or click dots.
- Show navigation affordances (arrows, dots) on all devices. "Swipe only" carousels fail discoverability tests.
- Use
loading="lazy"on images in non-visible slides to improve performance. - Provide enough visual cues that more content exists — peek the next slide's edge, use dot indicators, or show a counter.
- Pause autoplay on hover, focus, and any user interaction with the carousel.
- Use the "peek" variant for touch devices — partially visible adjacent slides are the strongest affordance for swipe navigation.
Don't:
- Don't use auto-playing carousels for critical content. Studies from NN/g show users treat auto-playing carousels like banner ads — they're ignored.
- Don't use carousels to avoid making content hierarchy decisions. If everything is "important," nothing is.
- Don't exceed 5–8 slides. Engagement drops dramatically beyond 3 slides — by slide 5, you're talking to < 2% of users.
- Don't use a carousel when a simple scrollable row of cards would work. Horizontal scroll with CSS
scroll-snap-typeis lighter and more accessible. - Don't disable swipe on touch devices — it's the most natural interaction pattern.
- Don't use excessively slow transitions. 300–500ms is the sweet spot. Anything over 700ms feels sluggish.
- Don't nest carousels (a carousel slide containing another carousel). It's a UX and accessibility nightmare.
Performance tip: For image-heavy carousels, load only the current slide and its immediate neighbors (slidesPerView + 2). Use IntersectionObserver or the loading="lazy" attribute. Decode images off-thread with img.decode() to avoid frame drops during transitions.
Code Snippets
<!-- Accessible Carousel -->
<section class="carousel"
role="region"
aria-roledescription="carousel"
aria-label="Customer testimonials">
<!-- Slide Container -->
<div class="carousel-viewport">
<div class="carousel-track" style="--slide-count: 4;">
<div class="carousel-slide" role="group"
aria-roledescription="slide" aria-label="Slide 1 of 4">
<blockquote>
<p>"This product changed everything for our team."</p>
<cite>— Jane Smith, Acme Corp</cite>
</blockquote>
</div>
<div class="carousel-slide" role="group"
aria-roledescription="slide" aria-label="Slide 2 of 4">
<blockquote>
<p>"Incredible performance and support."</p>
<cite>— John Doe, Beta Inc</cite>
</blockquote>
</div>
<!-- more slides... -->
</div>
</div>
<!-- Navigation Arrows -->
<button class="carousel-prev" aria-label="Previous slide">
<svg aria-hidden="true"><!-- left arrow --></svg>
</button>
<button class="carousel-next" aria-label="Next slide">
<svg aria-hidden="true"><!-- right arrow --></svg>
</button>
<!-- Dot Indicators -->
<div class="carousel-dots" role="tablist" aria-label="Slide navigation">
<button role="tab" aria-selected="true" aria-label="Go to slide 1"
class="carousel-dot carousel-dot--active"></button>
<button role="tab" aria-selected="false" aria-label="Go to slide 2"
class="carousel-dot"></button>
<button role="tab" aria-selected="false" aria-label="Go to slide 3"
class="carousel-dot"></button>
<button role="tab" aria-selected="false" aria-label="Go to slide 4"
class="carousel-dot"></button>
</div>
<!-- Live Region for Announcements -->
<div class="sr-only" aria-live="polite" aria-atomic="true">
Slide 1 of 4: "This product changed everything for our team."
</div>
</section>
<style>
.carousel-viewport {
overflow: hidden;
}
.carousel-track {
display: flex;
transition: transform 300ms ease-out;
}
.carousel-slide {
flex: 0 0 100%;
min-width: 0;
}
@media (prefers-reduced-motion: reduce) {
.carousel-track {
transition: none;
}
}
</style>// Carousel Component
import { useState, useCallback, useEffect, useRef } from 'react';
interface CarouselProps {
children: React.ReactNode[];
slidesPerView?: number;
spaceBetween?: number;
loop?: boolean;
autoplay?: false | { delay: number; pauseOnHover?: boolean };
navigation?: boolean;
pagination?: 'dots' | 'fraction' | false;
speed?: number;
ariaLabel: string;
onSlideChange?: (index: number) => void;
}
function Carousel({
children,
slidesPerView = 1,
spaceBetween = 0,
loop = false,
autoplay = false,
navigation = true,
pagination = 'dots',
speed = 300,
ariaLabel,
onSlideChange,
}: CarouselProps) {
const [current, setCurrent] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const totalSlides = children.length;
const liveRef = useRef<HTMLDivElement>(null);
const prefersReducedMotion = usePrefersReducedMotion();
const goTo = useCallback((index: number) => {
const next = loop
? (index + totalSlides) % totalSlides
: Math.max(0, Math.min(index, totalSlides - 1));
setCurrent(next);
onSlideChange?.(next);
}, [loop, totalSlides, onSlideChange]);
// Autoplay
useEffect(() => {
if (!autoplay || isPaused || prefersReducedMotion) return;
const timer = setInterval(() => goTo(current + 1), autoplay.delay);
return () => clearInterval(timer);
}, [autoplay, current, isPaused, goTo, prefersReducedMotion]);
// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowLeft') goTo(current - 1);
if (e.key === 'ArrowRight') goTo(current + 1);
};
const transitionDuration = prefersReducedMotion ? 0 : speed;
return (
<section
className="carousel"
role="region"
aria-roledescription="carousel"
aria-label={ariaLabel}
onKeyDown={handleKeyDown}
onMouseEnter={() => autoplay && setIsPaused(true)}
onMouseLeave={() => autoplay && setIsPaused(false)}
onFocus={() => autoplay && setIsPaused(true)}
>
<div className="carousel-viewport">
<div
className="carousel-track"
style={{
transform: `translateX(-${current * (100 / slidesPerView)}%)`,
transition: `transform ${transitionDuration}ms ease-out`,
gap: spaceBetween,
}}
>
{children.map((child, i) => (
<div
key={i}
className="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label={`Slide ${i + 1} of ${totalSlides}`}
style={{ flex: `0 0 ${100 / slidesPerView}%` }}
inert={i !== current ? '' : undefined}
>
{child}
</div>
))}
</div>
</div>
{navigation && (
<>
<button className="carousel-prev"
aria-label="Previous slide"
onClick={() => goTo(current - 1)}
disabled={!loop && current === 0}>
←
</button>
<button className="carousel-next"
aria-label="Next slide"
onClick={() => goTo(current + 1)}
disabled={!loop && current === totalSlides - 1}>
→
</button>
</>
)}
{pagination === 'dots' && (
<div className="carousel-dots" role="tablist" aria-label="Slide navigation">
{children.map((_, i) => (
<button key={i} role="tab"
aria-selected={i === current}
aria-label={`Go to slide ${i + 1}`}
className={`carousel-dot ${i === current ? 'active' : ''}`}
onClick={() => goTo(i)} />
))}
</div>
)}
{pagination === 'fraction' && (
<div className="carousel-fraction" aria-live="polite">
{current + 1} / {totalSlides}
</div>
)}
{autoplay && (
<button className="carousel-pause"
aria-label={isPaused ? 'Play slideshow' : 'Pause slideshow'}
onClick={() => setIsPaused(!isPaused)}>
{isPaused ? '▶' : '⏸'}
</button>
)}
<div ref={liveRef} className="sr-only" aria-live="polite" aria-atomic="true" />
</section>
);
}
// Usage
<Carousel ariaLabel="Customer testimonials" pagination="dots" speed={400}>
<TestimonialCard author="Jane Smith" quote="Changed everything." />
<TestimonialCard author="John Doe" quote="Incredible support." />
<TestimonialCard author="Alice Wang" quote="Best tool we've used." />
</Carousel>Design Systems
Swiper (swiperjs.com) is the de facto standard carousel library, used by millions of sites. It provides slidesPerView, spaceBetween, loop, autoplay, navigation, pagination (dots/fraction/progressbar/custom), effect (slide/fade/cube/coverflow/flip/creative), speed, breakpoints, freeMode, thumbs (thumbnail navigation), zoom, virtual (virtual rendering for hundreds of slides), and a11y (built-in ARIA attributes). Swiper's React wrapper (swiper/react) provides <Swiper> and <SwiperSlide> components. Transition effects are GPU-accelerated via CSS transforms. Use the Animation Tool and Transition Tool to match Swiper's easing to your design system.
Embla Carousel is a lightweight, extensible carousel library focused on performance and accessibility. It provides a headless core with plugins for autoplay, auto-scroll, auto-height, class names, fade, and wheel gestures. Embla's API is imperative (embla.scrollTo(), embla.scrollNext()) with event listeners (on('select'), on('scroll')). Its bundle size (~3KB gzipped) is significantly smaller than Swiper (~40KB). Embla is the recommended choice for custom design systems where you want full control over markup and styling.
Material Design 3 does not provide a dedicated Carousel component in MUI. MUI developers typically use Swiper, Embla, or react-slick. Google's Material Design guidelines include carousel specs with three variants: Multi-browse (shows multiple items with varying sizes), Uncontained (items extend to the screen edge), and Hero (single prominent item). These specs emphasize that carousels should reveal content gradually and respond to swipe gestures.
Ant Design provides a Carousel component (wrapping react-slick internally) with autoplay, dots, dotPosition (top/bottom/left/right), effect (scrollx/fade), afterChange, beforeChange, and all react-slick props via spread. Ant's implementation is mature but tightly coupled to react-slick's API.
Chakra UI and Radix UI do not provide carousel primitives. Chakra recommends composing with Embla or Swiper. Radix's stance is that carousels involve too many application-specific decisions (content type, navigation style, autoplay behavior) to standardize as a headless primitive.
Headless UI does not include a carousel component.
react-aria (Adobe) provides useCarousel hooks that handle keyboard navigation, ARIA attributes, and focus management without any visual implementation — ideal for fully custom carousels built on top of CSS scroll-snap. This is the most accessible-by-default approach.