Carousel
Free · MITReact Native · ExpoA 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/carousel.jsonThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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. | number | 0.84 |
| gap Space between cards in points. Default 12. | number | 12 |
| 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. | number | 0 |
| 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 | number | false |
| pagination Pill pagination under the cards. The active pill stretches and, with autoplay, fills with the timer. Default `true`. | boolean | true |
| 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
| Prop | Type | Default |
|---|---|---|
| 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. | number | 0.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.
Related
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.
A circular progress ring that fills on a spring, laps itself past 100 % with a gap that lifts the lap off the ring, and bounces once with a success haptic when it completes.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.