Carousel

A horizontally paged slide viewer: swipe (or use the prev/next arrows and the dot indicators) to move one slide at a time.
Paging snaps to the viewport width, the current slide drives the dots, and the arrows step the index (clamped, or wrapped when loop). Slides hold any content; pass an items array of { key, content }.
Each slide is itself the card: it paints the surface, rounds the corners and clips its content to them. So pass the slide's content, not a Card. A plain string renders in the slide's own type, inset from its edge; any other node fills the slide inside its 1px edge, so a picture is masked to the slide's shape. A Card nested inside frames the slide twice, and on Android the Material 3 item's 28dp corners cut the Card's own 12dp edge.
The prev and next arrows sit beside the slides, never over them, so nothing near a slide's edge is hidden behind an arrow; the slides narrow by the two arrow gutters instead.
Every slide is mounted from the first frame and stays mounted, so a slide's own state (a field's text, a playing video) survives the carousel measuring its viewport, on the first layout and whenever a hidden carousel is shown again. Until it has measured, the current slide fills the viewport on its own. A carousel does not window its slides, so a long gallery loads every slide's content up front.
On web, Tab visits the carousel in the order it reads: the previous arrow, the slide viewport, the next arrow, then the slide picker (an arrow disabled at the end of a non-looping carousel is skipped). Tab reaches the viewport while its slides overflow it, and the focused viewport draws the theme's focus ring around the slide. Left and Right move between slides, while Home and End reach the first and last. Controls inside a slide keep their own keyboard behavior. The named slide-picker buttons report the current slide and its position in the set; activating the current slide does nothing. Picker targets measure at least 24px on web, 44pt on iOS, and 48dp on Android, independently of the small painted dots.
The active dot follows the committed slide (a dot press, the arrows, the keyboard, a controlled index, a finished swipe) in both surface modes; the strip is absent with one slide, with showDots={false}, and by default on Android.
Slide 1
iOS
Slide 1
Android
Slide 1
Web
tsx
<Carousel
items={[
{ key: "one", content: "Slide 1" },
{ key: "two", content: "Slide 2" },
{ key: "three", content: "Slide 3" }
]}
/>

Props

Prop
Description
items
required
CarouselItem[]
The slides, left to right.
index
number
Controlled current slide. Pair with `onIndexChange`. Omit for uncontrolled.
defaultIndex
number
Initial current slide for the uncontrolled case (read once; default 0).
onIndexChange
(index: number) => void
Called with the next current index whenever the slide changes.
loop
boolean
Wrap from the last slide to the first (and first to last) on arrow nav.
showArrows
boolean
Show the prev/next chevron buttons beside the slides. Default is platform-adaptive: on for web, off for iOS + Android (swipe idioms); pass `true` to opt in.
showDots
boolean
Show the centered dot indicators below the slides. Default is platform-adaptive: on for web + iOS (UIPageControl), off for Android M3.
testID
string
E2E hook forwarded to the root element.
style
LayoutStyle
Composition within a parent only, never a restyle hook and never a width: the parent layout container provides the bounds.

Do & Don’t

Current slide

Don’t
Slide 1
Slide 2
Stack the slides yourself with a manual row of pressables; it loses the snap paging, the synced dots, and the per-slide accessibility.
Do
Step 1
Keep one current slide and let the dots mirror it, so the position in the set is always clear.