Carousel
A hand-drawn scroll-snap carousel with prev/next controls, built on native scrolling.
Overview
Carousel is a slideshow of horizontally (or vertically) scrolling slides, built on native CSS
scroll-snap — no third-party engine. Use it to browse a set of peer items one viewport at a time:
a photo gallery, a set of feature cards, an onboarding sequence. Compose it from CarouselContent,
CarouselItem, CarouselPrevious, and CarouselNext.
Anatomy
A role="region" labelled as a carousel wrapping a scroll-snap track (CarouselContent) of slides
(CarouselItem, each role="group" described as a slide). CarouselPrevious / CarouselNext are
Buttons that move one viewport per step and disable at the start / end respectively. Arrow keys
scroll along the orientation axis; the scrollbar is visually hidden while snapping stays native.
Variants & States
The only variant axis is orientation ('horizontal' default, or 'vertical'). The control states
come from scroll position.
| State | Status | Notes |
|---|---|---|
| At start | Implemented | CarouselPrevious is disabled when the viewport is at the start. |
| At end | Implemented | CarouselNext is disabled when the viewport is at the end. |
| Focus | Implemented | The region is arrow-key scrollable when focused. |
| Disabled | Not implemented | The carousel itself has no disabled state. |
| Loading | Not implemented | — |
Usage
Do
- Give the carousel (or a wrapper) an explicit width — and, for a vertical carousel, a bounded height on CarouselContent.
- Provide an aria-label describing the set of slides (e.g. "Photos").
- Keep one slide per viewport unless you deliberately override flexBasis on CarouselItem.
Don't
- Don't put essential, time-sensitive content in a carousel — users may never scroll past the first slide.
- Don't auto-advance without a pause control; motion that can't be stopped is an accessibility problem.
Accessibility
- The root is the ARIA APG carousel pattern:
role="region"+aria-roledescription="carousel", named by youraria-label. Each slide is arole="group"witharia-roledescription="slide". - The controls are real
<button>s labelled “Previous slide” / “Next slide” and becomedisabledat the respective ends, so their state is announced. - Arrow keys (Left/Right when horizontal, Up/Down when vertical) scroll the region, and the native scroll container remains keyboard- and touch-operable.
- See the Accessibility Guide for shared conventions.
Content
Slides should be peers of roughly equal weight. Keep each slide self-contained — a user might land on any one first — and avoid splitting a single message across slides.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Scroll axis (Carousel). |
...rest | HTMLAttributes<HTMLDivElement> | — | Native <div> attributes (aria-label, etc.) pass through to the region. |
CarouselContent | HTMLAttributes<HTMLDivElement> | — | The scroll-snap track; set maxHeight/maxWidth via style to bound it. |
CarouselItem | HTMLAttributes<HTMLDivElement> | — | A single slide; override flexBasis via style for multiple-per-view. |
CarouselPrevious / CarouselNext | ButtonProps | — | The prev/next controls (Button); auto-disable at the ends. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Reflected attribute; scroll axis. |
slides | <gh-carousel-item> | — | Place <gh-carousel-item> slides in the default slot. Import both @ghds/web-components/carousel and /carousel-item. |
controls | built-in | — | The element renders its own prev/next buttons and indicator dots — no separate control elements. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Scroll axis (Carousel). |
accessibilityLabel | string | — | Accessible label for the carousel region. |
children | ReactNode | — | Compose with CarouselContent / CarouselItem / CarouselPrevious / CarouselNext / CarouselIndicators. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from '@ghds/react/carousel';
<Carousel orientation="horizontal" aria-label="Photos">
<CarouselContent>
<CarouselItem>Slide 1</CarouselItem>
<CarouselItem>Slide 2</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>;
<!-- Web Components — @ghds/web-components (renders its own prev/next controls + dots) -->
<script type="module">
import '@ghds/web-components/carousel';
import '@ghds/web-components/carousel-item';
</script>
<gh-carousel aria-label="Photos">
<gh-carousel-item>Slide 1</gh-carousel-item>
<gh-carousel-item>Slide 2</gh-carousel-item>
</gh-carousel>
// React Native — @ghds/react-native
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
CarouselIndicators,
} from '@ghds/react-native/carousel';
<Carousel accessibilityLabel="Photos">
<CarouselContent>
<CarouselItem>{/* slide 1 */}</CarouselItem>
<CarouselItem>{/* slide 2 */}</CarouselItem>
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
<CarouselIndicators />
</Carousel>;
Live Demo
React and Web Components render live below, driven by the same tokens — toggle dark mode in the
header to see both respond identically. React Native can’t run in a browser without additional
react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.
(The Web Components element renders its own prev/next controls and indicator dots.)