Carousel
Slideshow with bar indicators, keyboard navigation and an accessible pause control.
Introduction
A carousel rotates through a small set of slides. UX4G specifies chevrons inside the left and right edges, a caption over the lower part of the slide, bar indicators beneath it, and a pause control for when it advances on its own.
Carousels are easy to build badly. Most of this component is the part that stops it moving.
Installation
npx shadcn@latest add https://indiacn.in/r/carousel.jsonimport {
Carousel,
CarouselContent,
CarouselItem,
CarouselCaption,
CarouselPrevious,
CarouselNext,
CarouselIndicators,
CarouselPlayPause,
} from '@/components/ui/carousel';Examples
Default
Apply online
Start a new application from any device.
Track progress
Follow every stage with a reference number.
Collect your document
Download it or pick it up at a centre.
import {
Carousel,
CarouselCaption,
CarouselContent,
CarouselIndicators,
CarouselItem,
CarouselNext,
CarouselPlayPause,
CarouselPrevious,
} from '@/components/ui/carousel';
import { Headline4 } from '@/components/ui/typography';
const SLIDES = [
{ title: 'Apply online', body: 'Start a new application from any device.' },
{ title: 'Track progress', body: 'Follow every stage with a reference number.' },
{ title: 'Collect your document', body: 'Download it or pick it up at a centre.' },
];
export default function Component() {
return (
<Carousel className='w-full max-w-[640px]' label='How it works'>
<CarouselContent>
{SLIDES.map(slide => (
<CarouselItem key={slide.title}>
<div className='flex h-[280px] items-center justify-center bg-neutral-50'>
<Headline4 className='text-neutral-400'>{slide.title}</Headline4>
</div>
<CarouselCaption title={slide.title}>{slide.body}</CarouselCaption>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
<CarouselIndicators />
</Carousel>
);
}Over dark slides
variant='dark' switches the controls, caption and indicators to white. It describes the slide behind the controls, not the page theme — a photographic slide is dark whichever theme the site is in. This mirrors the Dark property in the Figma kit.
Apply online
Start a new application from any device.
Track progress
Follow every stage with a reference number.
Collect your document
Download it or pick it up at a centre.
import {
Carousel,
CarouselCaption,
CarouselContent,
CarouselIndicators,
CarouselItem,
CarouselNext,
CarouselPlayPause,
CarouselPrevious,
} from '@/components/ui/carousel';
import { Headline4 } from '@/components/ui/typography';
const SLIDES = [
{ title: 'Apply online', body: 'Start a new application from any device.' },
{ title: 'Track progress', body: 'Follow every stage with a reference number.' },
{ title: 'Collect your document', body: 'Download it or pick it up at a centre.' },
];
export default function Component() {
return (
<Carousel variant='dark' className='w-full max-w-[640px]' label='How it works, dark slides'>
<CarouselContent>
{SLIDES.map(slide => (
<CarouselItem key={slide.title}>
<div className='bg-neutral flex h-[280px] items-center justify-center'>
<Headline4 className='text-neutral-0/40'>{slide.title}</Headline4>
</div>
<CarouselCaption title={slide.title}>{slide.body}</CarouselCaption>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
<CarouselIndicators />
</Carousel>
);
}Advancing on its own
Apply online
Start a new application from any device.
Track progress
Follow every stage with a reference number.
Collect your document
Download it or pick it up at a centre.
import {
Carousel,
CarouselCaption,
CarouselContent,
CarouselIndicators,
CarouselItem,
CarouselNext,
CarouselPlayPause,
CarouselPrevious,
} from '@/components/ui/carousel';
import { Headline4 } from '@/components/ui/typography';
const SLIDES = [
{ title: 'Apply online', body: 'Start a new application from any device.' },
{ title: 'Track progress', body: 'Follow every stage with a reference number.' },
{ title: 'Collect your document', body: 'Download it or pick it up at a centre.' },
];
export default function Component() {
return (
<Carousel
autoPlayInterval={4000}
className='w-full max-w-[640px]'
label='How it works, advancing automatically'
>
<CarouselContent>
{SLIDES.map(slide => (
<CarouselItem key={slide.title}>
<div className='flex h-[280px] items-center justify-center bg-neutral-50'>
<Headline4 className='text-neutral-400'>{slide.title}</Headline4>
</div>
<CarouselCaption title={slide.title}>{slide.body}</CarouselCaption>
</CarouselItem>
))}
</CarouselContent>
<CarouselPlayPause />
<CarouselPrevious />
<CarouselNext />
<CarouselIndicators />
</Carousel>
);
}Accessibility
Anything that moves by itself is a barrier unless it can be stopped, so:
CarouselPlayPauseis the WCAG 2.2.2 stop control. Render it whenever you passautoPlayInterval.- Auto-advance pauses on hover and on keyboard focus, and resumes when you leave. Reading a slide should not be a race.
- Auto-advance never starts under
prefers-reduced-motion: reduce.CarouselPlayPausehides itself in that case, because there is nothing to pause. - Left and right arrow keys move between slides while focus is inside the carousel.
- The region carries
aria-roledescription="carousel", each slidearia-roledescription="slide", and the track is anaria-live="polite"region so a slide change is announced without interrupting. - Indicators are real buttons labelled "Go to slide N", with
aria-currenton the current one.
Measurements
Read off the Figma symbols:
| value | |
|---|---|
| indicator bar | 30 × 3px, 6px gap |
| inactive indicator | current colour at 50% |
| active indicator | current colour at 100% |
| chevron | 32px |
The 50% is measured, not assumed: on the dark slide the inactive bar reads #bbbbbb, which is white at 50% over the #777777 slide; on the light slide it reads #7a7a7a, which is black at 50% over #f5f5f5.
Guidelines
- Three to five slides. A carousel is not a place to hide content — most people never reach slide four.
- Never put something a person must see in a slide other than the first.
- Give every slide a caption. A slide that only makes sense as an image excludes anyone who cannot see it.
API Reference
Carousel
| Prop | Type | Default | Description |
|---|---|---|---|
variant | light | dark | light | Tone of the slide behind the controls |
autoPlayInterval | number | — | Milliseconds between slides. Omit for manual only |
label | string | 'Carousel' | Accessible name for the region |
CarouselCaption
| Prop | Type | Description |
|---|---|---|
title | ReactNode | Bold first line |
children | ReactNode | Supporting line |
Other parts
CarouselContent wraps the slides and counts them. CarouselItem is one slide. CarouselPrevious, CarouselNext, CarouselIndicators and CarouselPlayPause take no required props — they read the carousel's state from context.