BetaData display

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.

PropTypeRequiredDescription
sizeSize | undefinedNoThe arrows' and the dots' size, the gap between slides, the controls' corner.
variantCarouselVariant | undefinedNo
colorstring | undefinedNoA raw value (R7) for the controls. The slides are yours and it never reaches them.
radiusRadiusKey | undefinedNoThe slides' corner.
indexnumber | undefinedNoWhich slide is shown. Present means controlled.
defaultIndexnumber | undefinedNoWhere it starts when uncontrolled.
onIndexChange((index: number) => void) | undefinedNo
itemsPerViewnumber | undefinedNoHow many whole slides are in view at once.
peeknumber | undefinedNoHow 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.
gapnumber | undefinedNoBetween two slides, in points. Defaults to what `size` says.
hasLoopboolean | undefinedNoWhether the arrows wrap round the ends rather than stopping at them.
autoPlayIntervalnumber | undefinedNoAdvance 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.
isDisabledboolean | undefinedNo
styleStyleProp<ViewStyle>No
childrenReactNodeNo

Slots

CarouselControlProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNoAn arrow of your own — an icon, a word. Unset draws the built-in chevron.
styleStyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)NoR9 — `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.

PropTypeRequiredDescription
indexnumberYesWhich slide it stands for.
childrenReactNodeNo

CarouselTextSlotProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

CarouselThumbnailProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
indexnumberYesWhich slide pressing it goes to.
childrenReactNodeNo

CarouselViewSlotProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

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.

variantthe arrows
primaryon surface
secondaryon default
tertiaryon surface, with a border
ghostno 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

This component declares no rule of its own. The migration guide covers variants, colours and slots.

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

slotwhat it is
Carousel.Contentthe track
Carousel.Itemone slide
Carousel.Previousback one, over the slides
Carousel.Nexton one
Carousel.Indicatorthe row of dots
Carousel.Dotone of them, for an indicator you compose yourself
Carousel.Counter3 / 12
Carousel.Thumbnailsa strip of small versions of the slides
Carousel.Thumbnailone 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.