Carousel

Free · MITReact Native · Expo

A snapping card carousel that follows the finger 1:1, shrinks and dims the neighbours, stretches its pagination pill with the drag, and turns that pill into a timer when it autoplays.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/carousel.json

Then run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/carousel.

Usage

import { View } from "react-native";
import { Carousel, CarouselParallax } from "@/components/goodui-native/carousel/carousel";
import { Text } from "@/components/goodui-native/foundation/text";

type Stay = { id: string; title: string; price: string };

export function FeaturedStays({ stays, open }: { stays: Stay[]; open: (s: Stay) => void }) {
  return (
    <Carousel
      data={stays}
      height={200}
      keyExtractor={(s) => s.id}
      getItemLabel={(s) => s.title}
      accessibilityLabel="Featured stays"
      onItemPress={(s) => open(s)}
      renderItem={({ item }) => (
        <View style={{ flex: 1, justifyContent: "flex-end", padding: 16 }}>
          <Text size="title3" weight="medium">{item.title}</Text>
          <Text tone="secondary">{item.price} a night</Text>
        </View>
      )}
    />
  );
}

When to use

Use it for
  • A few featured items people browse one at a time: stays, plans, offers, onboarding steps, cards in a wallet.
  • Content where a peek of the next card invites a swipe.
  • Promotions that rotate on their own (autoplay) but stop as soon as someone touches them.
Reach for something else
  • Long or searchable collections: use a vertical list or a grid.
  • Switching between views of the same data: use segmented-control.
  • Content everyone must see: a carousel hides everything but the first card.

Examples

Autoplay with a parallax background

<Carousel
  data={offers}
  autoplay={5000}
  height={180}
  renderItem={({ item }) => (
    <>
      <CarouselParallax>
        <Image source={item.image} style={{ flex: 1 }} />
      </CarouselParallax>
      <Text style={{ margin: 16 }}>{item.title}</Text>
    </>
  )}
/>

Controlled, full width, flat

const [step, setStep] = useState(0);

<Carousel data={steps} index={step} onIndexChange={setStep} itemWidth={1} gap={0} variant="flat" renderItem={...} />

API reference

Carousel

PropTypeDefault
data*
Items, one card each. Built for a handful to a few dozen cards.
T[]—
renderItem*
Renders a card. Do not put Pressables inside: use `onItemPress`, which works with dragging on every platform.
(info: { item: T; index: number }) => ReactNode—
keyExtractor
Stable key per item. Defaults to the index.
(item: T, index: number) => string—
itemWidth
Card width as a fraction of the carousel width, 0.5 to 1. Neighbours peek in the rest. Default 0.84.
number0.84
gap
Space between cards in points. Default 12.
number12
height
Card height in points. Omit to size cards by their content.
number—
variant
`scale` shrinks and dims the neighbours as they leave the center; `flat` keeps every card the same.
"scale" | "flat""scale"
index
The centered card (controlled). Pair with `onIndexChange`.
number—
defaultIndex
The first centered card (uncontrolled). Default 0.
number0
onIndexChange
Called when a different card settles in the center.
(index: number) => void—
autoplay
Advance on a timer: `true` for every 4 s, or a number of milliseconds. Pauses while touched or hovered, off under reduced motion.
boolean | numberfalse
pagination
Pill pagination under the cards. The active pill stretches and, with autoplay, fills with the timer. Default `true`.
booleantrue
onItemPress
Called when the centered card is tapped. Tapping a neighbour brings it to the center instead.
(item: T, index: number) => void—
haptic
Haptic when a swipe or a pill changes the card. `false` turns it off.
HapticName | false"select"
getItemLabel
Short description of a card for screen readers, for example its title.
(item: T, index: number) => string—
accessibilityLabel
Name of the carousel, for example "Featured stays".
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<CarouselLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

CarouselParallax

PropTypeDefault
children*
A background layer, usually an image or a color field, that fills the card.
ReactNode—
strength
How far the layer lags behind the card, as a fraction of the card width. Default 0.18.
number0.18

Motion and haptics

  • Cards follow the finger 1:1 and settle on the smooth spring with the release velocity; a fling moves one card at most.
  • Past the first or last card the drag meets growing resistance.
  • variant='scale' shrinks neighbours to 0.9 and dims them as they leave the center, continuously with the drag.
  • The pagination pill stretches from the old card to the new one with the drag; with autoplay the active pill fills over the interval.
  • CarouselParallax layers lag behind their card by a fraction of its width.
  • Reduced motion: release settles on a short timing curve, programmatic changes jump, parallax stays still and autoplay is off.
  • Haptic: select when a swipe, a tap on a neighbour or a pill changes the card (configurable with the haptic prop); none for autoplay
  • Position, scale, dimming, pills and parallax run on the UI thread from one shared value; React renders only when the centered card changes.
  • Every card is rendered: keep it to a few dozen cards.

Accessibility

  • The carousel is one adjustable element: swipe up or down with a screen reader to move between cards.
  • Its value reads '2 of 5' plus getItemLabel for the centered card; with onItemPress, activating it presses that card.
  • Every pagination pill is a button labelled 'Go to card n' with a 44 pt tall touch target.
  • Autoplay is off under reduced motion and pauses while a finger or pointer rests on the cards.
  • On the web it is a focusable slider with aria-valuenow (card number) and aria-valuetext; arrow keys move between cards and Enter presses the centered card.

Notes for AI

  • Do not put Pressable or buttons inside cards: they swallow the drag on the web. Use onItemPress for the centered card.
  • Give cards a height or content with a height; the cards clip to a rounded surface, so do not add another card around the content.
  • itemWidth is a fraction of the carousel width (0.84 by default), not points.
  • With a controlled index, update it from onIndexChange, or autoplay and swipes cannot move it.
  • CarouselParallax only works inside a Carousel card; put text after it so the text sits on top.