IndiaCN UI

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.json
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselCaption,
CarouselPrevious,
CarouselNext,
CarouselIndicators,
CarouselPlayPause,
} from '@/components/ui/carousel';

Examples

Default

Apply online

Apply online

Start a new application from any device.

Track progress

Track progress

Follow every stage with a reference number.

Collect your document

Collect your document

Download it or pick it up at a centre.

CarouselDefault
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

Apply online

Start a new application from any device.

Track progress

Track progress

Follow every stage with a reference number.

Collect your document

Collect your document

Download it or pick it up at a centre.

CarouselDark
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

Apply online

Start a new application from any device.

Track progress

Track progress

Follow every stage with a reference number.

Collect your document

Collect your document

Download it or pick it up at a centre.

CarouselAutoPlay
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:

  • CarouselPlayPause is the WCAG 2.2.2 stop control. Render it whenever you pass autoPlayInterval.
  • 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. CarouselPlayPause hides 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 slide aria-roledescription="slide", and the track is an aria-live="polite" region so a slide change is announced without interrupting.
  • Indicators are real buttons labelled "Go to slide N", with aria-current on the current one.

Measurements

Read off the Figma symbols:

value
indicator bar30 × 3px, 6px gap
inactive indicatorcurrent colour at 50%
active indicatorcurrent colour at 100%
chevron32px

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

PropTypeDefaultDescription
variantlight | darklightTone of the slide behind the controls
autoPlayIntervalnumberMilliseconds between slides. Omit for manual only
labelstring'Carousel'Accessible name for the region

CarouselCaption

PropTypeDescription
titleReactNodeBold first line
childrenReactNodeSupporting 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.

On this page