Carousel

A set of slides shown one (or a few) at a time with previous/next controls, a slide picker, and optional auto-rotation that pauses the moment anyone touches it — the APG carousel with the accessibility built in rather than bolted on.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Carousel } from '@design-schema/react';

Examples

Props

9 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

What the carousel shows ("Featured products", "Customer stories").

Accessibility: aria-label on the region; each slide is named `copy.slideLabel` ("2 of 4") alone — its heading is read as content, never joined into the name.

web, lit, rn, swiftui
children requiredcontent—

One `CarouselSlide` per slide. A slide is any content; a Card is the usual shape. Slides should be equal height.

web, lit, rn, swiftui
perViewnumber1

How many slides are visible at once at the widest layout. The page size is `perView` while the viewport's own width is above `layout.maxWidth.prose` and 1 at or below it, measured on the viewport (ResizeObserver on web and Lit, onLayout on React Native); `perView` applies until the first measurement. The comparison needs the token as a number: web and Lit read it from its built custom property on the viewport (px and rem both parsed), React Native from the theme; when it cannot be read, `perView` stands as given.

web, lit, rn, swiftui
loopbooleanfalse

Next from the last returns to the first. Off by default so users can tell where the end is.

web, lit, rn, swiftui
autoplaybooleanfalse

Rotate automatically every `interval`. Starts only when the user has not asked for reduced motion; stops on hover, focus, touch, or the play/pause button; never restarts on its own after the user pauses it.

web, lit, rn, swiftui
intervalnumber6000

Milliseconds between automatic advances; values below 5000 are raised to 5000 in every build on every platform, with a development warning once ever per instance — a carousel that is given a valid `interval` and then an invalid one again does not warn a second time — and only while `autoplay` is on.

web, lit, rn, swiftui
pickerdots | tabs | nonedots

How slides are chosen directly: small dot buttons, tabs with each slide's label (for few, meaningful slides), or none (arrows only).

web, lit, rn, swiftui
activeIndexnumber—

Controlled current slide (zero-based). Omit for uncontrolled.

web, lit, rn, swiftui
snapbooleantrue

Swiping or scrolling snaps to slide boundaries.

web, lit, rn, swiftui

Events

1 row
EventTypeDescriptionPlatforms
onChangeonChange / change

Fired when the current slide changes, with the new index and the reason (`next`, `prev`, `picker`, `swipe`, `autoplay`). The programmatic cases are autoplay and a swipe settling; a change of a controlled `activeIndex` never fires it. A scroll counts as `swipe` only when the user started it (web and Lit: after pointerdown, touchstart or wheel on the viewport; React Native: between onScrollBeginDrag and onMomentumScrollEnd, and also at onScrollEndDrag, since momentum is only reported after the drag has ended — the change settles there and the swipe window reopens in onMomentumScrollBegin when a drag had just ended, so a flick that carries further reports again where it lands, and a controlled carousel simply scrolls back twice to the same offset; a programmatic animated scroll clears the drag flag first, so its own momentum events are never read as a swipe) — a scroll started by an arrow, the picker or autoplay reports its own reason once. A swipe settles on the viewport's `scrollend` event where the engine has it and on the next IntersectionObserver delivery otherwise, never on a timer.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

If you wire up the gesture events, provide the pointer and keyboard path to the same outcome.

Required props that carry accessibility meaning: label. Each is described in the props table above.

Implicit ARIA role: region.

Anatomy

regionviewporttrackslidecontrolSurfaceprevButtonnextButtonpickerpickerItemplayButtonliveRegion

What generation guarantees

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).

Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.

Live region — Changes are announced through a live region, without moving focus.

Gesture alternative — Every gesture — swipe, drag, long-press — has a single-pointer and a keyboard equivalent.

No hover-only content — Nothing is reachable by hover alone; keyboard focus reveals the same content.

Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).

Carousel keyboard support

6 rows
KeyAction
TabMoves through the controls (play/pause, previous, next, picker), then the viewport — a scroll container is a tab stop of its own, with a focus ring but no role and no name, so the region's name is not read twice — and then into the current slide's focusable content; hidden slides are inert.
ArrowRightNext slide (one slide, not one page): focus and selection move together with reason `picker`, wrapping at the ends as Tabs does whatever `loop` says. Dots and tabs both use a roving tabindex, so the picker is one tab stop. (focus on picker or in a tabs picker)
ArrowLeftPrevious slide, wrapping the same way. (focus on picker)
HomeFirst slide. (focus on picker)
EndLast slide. (focus on picker)
Enter or SpaceActivates the focused control: previous, next, a picker item, or play/pause.

Contrast pairs

5 rows
ForegroundBackgroundLevel
color.foregroundcolor.overlay.surfaceAA
color.control.selectedBackgroundcolor.backgroundAA (non-text)
color.border.strongcolor.backgroundAA (non-text)
color.foreground.mutedcolor.backgroundAA
color.foreground.strongcolor.backgroundAA

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.