Carousel
A series of slides, one or a few at a time, with the controls to move between them.
Overview
A series of slides, one or a few at a time, with the controls to move between them.
First example
<Carousel autoPlayInterval={2500}><Carousel.Content>{photos.map(photo => (<Carousel.Item key={photo.id}><Image source={photo.source} style={{ width: '100%', height: 200 }} /></Carousel.Item>))}</Carousel.Content><Carousel.Previous accessibilityLabel="Photo précédente" /><Carousel.Next accessibilityLabel="Photo suivante" /><Carousel.Indicator /></Carousel>
Anatomy
<Carousel>
<Carousel.Control />
<Carousel.Dot />
<Carousel.TextSlot />
<Carousel.Thumbnail />
<Carousel.ViewSlot />
</Carousel>
Usage
<Carousel autoPlayInterval={2500}>
<Carousel.Content>
{photos.map(photo => (
<Carousel.Item key={photo.id}>
<Image source={photo.source} style={{ width: '100%', height: 200 }} />
</Carousel.Item>
))}
</Carousel.Content>
<Carousel.Previous accessibilityLabel="Photo précédente" />
<Carousel.Next accessibilityLabel="Photo suivante" />
<Carousel.Indicator />
</Carousel>
Root props
CarouselProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| size | Size | undefined | No | The arrows' and the dots' size, the gap between slides, the controls' corner. |
| variant | CarouselVariant | undefined | No | — |
| color | string | undefined | No | A raw value (R7) for the controls. The slides are yours and it never reaches them. |
| radius | RadiusKey | undefined | No | The slides' corner. |
| index | number | undefined | No | Which slide is shown. Present means controlled. |
| defaultIndex | number | undefined | No | Where it starts when uncontrolled. |
| onIndexChange | ((index: number) => void) | undefined | No | — |
| itemsPerView | number | undefined | No | How many whole slides are in view at once. |
| peek | number | undefined | No | How much of the neighbouring slide shows at each edge, in points. Zero — the default — is a carousel whose slides fill the track. Anything above it is the promise that there is more to the side, which is worth more than the pixels it costs on a series nobody would otherwise scroll. |
| gap | number | undefined | No | Between two slides, in points. Defaults to what `size` says. |
| hasLoop | boolean | undefined | No | Whether the arrows wrap round the ends rather than stopping at them. |
| autoPlayInterval | number | undefined | No | Advance on its own every this many milliseconds. Unset is off. **It stops at the first interaction and does not come back.** A carousel that resumes moving under a reader who has taken hold of it is a carousel fighting them. |
| isDisabled | boolean | undefined | No | — |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
CarouselControlProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | An arrow of your own — an icon, a word. Unset draws the built-in chevron. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
CarouselDotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| index | number | Yes | Which slide it stands for. |
| children | ReactNode | No | — |
CarouselTextSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
CarouselThumbnailProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| index | number | Yes | Which slide pressing it goes to. |
| children | ReactNode | No | — |
CarouselViewSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Variants
The four emphases, narrowed as Card and Surface narrow them: a carousel reports nothing —
it is a way of arranging what does — so success or danger would be an intent nothing here
has.
| variant | the arrows |
|---|---|
primary | on surface |
secondary | on default |
tertiary | on surface, with a border |
ghost | no ground at all |
The arrows sit on a slide, so their ground is the raised one rather than the page's: a
control the same colour as the page vanishes over a pale photograph. ghost is what a
carousel over a dark photograph wants and what one over a white photograph must not use.
color lands where the variant put its roles — the active pill and the chevron. The dots at
rest keep their neutral fill: they are the ground the pill travels on, and a pale tint of the
accent there reads as a control that has half failed to load.
Size
size moves the arrows, the dots, the gap between slides, the thumbnails and the counter's
type — never a slide. A slide's width is the track's; its height is whatever is in it.
Accessibility
The arrows are buttons and carry accessibilityState.disabled at the ends. The dots and the
thumbnails carry accessibilityValue — position and total — and accessibilityState.selected.
What is in a slide or a thumbnail is yours, so an image with its own accessibilityLabel is
how a slide says what it shows.
Migration from legacy
Implementation notes
The slides are yours and the controls are the library's
variant, color and the palette reach the arrows, the dots and the counter. Nothing
here paints what is inside a Carousel.Item — a carousel of photographs and a carousel of
cards want opposite things there, and the caller is the one who knows which this is.
That is also why there is no renderItem and no data: the slides are children, in JSX
order, and what is in one is whatever you put there.
A slide's width comes from the track's, never from a prop
itemsPerView says how many whole slides are in view; peek says how much of the next one
shows at each edge. carouselMetrics divides the measured track between them:
├ peek ┼ gap ┼──── item ────┼ gap ┼──── item ────┼ gap ┼ peek ┤
A carousel whose slide width is given in points is a carousel that is wrong on the next
screen size — so it is not a prop, and the arithmetic lives in utils/carousel.ts where it
is read and tested rather than in the component where it is neither.
The peek brings a gap of its own. The run it costs at each edge is peek + gap, not
peek; without that second term the peeking neighbour touches the slide in view, and a
carousel whose slides touch reads as one wide image that has been cut.
Everything is clamped at zero. A track too narrow for the gaps and the peeks it was asked for gives slides of no width rather than negative ones.
Every control is a slot, not a prop
| slot | what it is |
|---|---|
Carousel.Content | the track |
Carousel.Item | one slide |
Carousel.Previous | back one, over the slides |
Carousel.Next | on one |
Carousel.Indicator | the row of dots |
Carousel.Dot | one of them, for an indicator you compose yourself |
Carousel.Counter | 3 / 12 |
Carousel.Thumbnails | a strip of small versions of the slides |
Carousel.Thumbnail | one of them |
A carousel that wants dots and no arrows leaves the arrows out. There is no
showArrows={false}, and there is no order to respect — the indicator above the track is a
<Carousel.Indicator /> written first.
The arrows are over the slides
An arrow in the flow would move the track it belongs to, and then the two would disagree about where the middle is. They are centred on the track's own measured height, so a carousel with dots under it does not have its arrows sitting low.
At the end of a series that does not loop an arrow stays in place and goes quiet — disabled, not removed. A control that disappears at the last slide takes its width with it and shifts everything beside it.
They need an accessibilityLabel, and there is a development warning when one is missing. A
chevron is not text, and unlike a labelled button there is nothing beside it to fall back on
— and no default, because which language it would be in is not this library's to decide.
The dot is the pill
The chosen dot grows in the row rather than being covered by a wider node over it. An overlay keeps the row's width fixed, so the pill spills over the dots either side and the reader sees a lozenge with a grey dot half under it.
It follows the drag, not the settle: the width and the colour are interpolated from the live scroll offset on the UI thread, so the pill grows out of one dot and into the next while the finger is still down. That is what makes a reader believe the indicator is attached to the thing they are moving.
A screen reader hears "3 of 5" through accessibilityValue rather than a label, for the
same reason the arrows have no default one.
The thumbnails show what they point at
The one thing dots cannot do: on a series of photographs a reader picks the one they want rather than counting across to it. The ring around the chosen one is always drawn and only its colour moves — a border that appears on selection nudges every thumbnail after it along the strip.
The index
index / defaultIndex / onIndexChange, controlled or not, as everywhere in the library.
It changes as the track crosses the halfway point, not when it stops. onMomentumScrollEnd
would be the obvious place and it is the wrong one twice: it never fires for a wheel or a
trackpad, so under the web renderer the index would never move at all; and it lands after the
fact, so the arrows and the thumbnails would sit on the previous slide for the length of the
deceleration.
A fast flick across three slides therefore reports three changes. That is what happened.
How a move is animated
A drag is the platform's: snapToInterval plus decelerationRate="fast", so a flick
lands on a slide rather than between two.
An arrow, a dot or an autoplay tick is a hand-run tween — the offset is carried from
where it is to the next step over ~420ms against an ease-out curve, a frame at a time,
so the track leaves fast under the press and brakes onto the slide. scrollTo({ animated: true }) was the obvious call and it is close to linear on both renderers: it leaves and
arrives at the same speed and the move reads as a jump cut. A press mid-travel eases on
from the live position rather than racing the tween before it.
`hasLoop` is about the arrows
Unset, they stop at the ends. Set, they wrap.
Autoplay always wraps, whatever hasLoop says. That prop answers "what does the arrow do
at the last slide"; an autoplay that stopped there would be one that quietly dies.
`autoPlayInterval` stops at the first interaction
And does not come back. A carousel that resumes moving under a reader who has taken hold of it is a carousel fighting them. A drag, an arrow, a dot or a thumbnail all count.
It is a number of milliseconds rather than a hasAutoPlay beside an interval, because the two
would only ever be set together.