All components
0:00/0:00

Recorded on iPhone. Swiped in portrait, then rotated to landscape, where more pages fan out along each edge.

Gesture

Rolling Carousel

Posters ride a surface that rolls away at the screen edges, like film around a spool.

Features

  • Cards roll around both edges of the screen, like film over a spool
  • A quick swipe keeps its momentum and glides to a stop
  • Tap the left or right side to move one card; tap fast to skip ahead
  • Can loop forever and play through the cards on its own
  • Choose a soft, sharp or default curve, or fine-tune every detail
  • Photos show in full at their own shape, never cropped

Built with

  • Skia Vertices
  • Reanimated worklets
  • Gesture Handler
FreeGitHub

Installation

1Install dependencies
npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets react-native-gesture-handler
2Wrap your app in GestureHandlerRootView
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Stack />
    </GestureHandlerRootView>
  );
}
3Run a development build (Expo Go can't load Skia)
npx expo run:ios   # or: npx expo run:android

Usage

import { RollingCarousel } from '@/components/rolling-carousel';

const posters = [require('./a.jpg'), require('./b.jpg'), 'https://…/c.jpg'];

export function Gallery() {
  return <RollingCarousel images={posters} style={{ height: 460 }} />;
}

Examples

Presets

Change the shape of the fold, then override any single value.

<RollingCarousel images={posters} preset="soft" />

<RollingCarousel images={posters} preset="soft" config={{ foldAngle: 85 }} />

Three across, looping, auto-play

Several cards face-on, an endless strip, and a page every 2.5 seconds.

<RollingCarousel
  images={products}
  loop
  autoPlay
  style={{ height: 320 }}
  config={{ cardsPerView: 3, gap: 12, autoPlayInterval: 2500 }}
/>

Driven from outside

Your own buttons and a page counter, with tapping turned off.

function PhotoGallery() {
  const carousel = useRef<RollingCarouselHandle>(null);
  const [page, setPage] = useState(0);

  return (
    <View>
      <RollingCarousel
        ref={carousel}
        images={photos}
        onIndexChange={setPage}
        tapNavigation={false}
      />

      <View style={styles.controls}>
        <Button title="Back" onPress={() => carousel.current?.previous()} />
        <Text>
          {page + 1} / {photos.length}
        </Text>
        <Button title="Next" onPress={() => carousel.current?.next()} />
      </View>
    </View>
  );
}

Props

PropTypeDefaultDescription
imagesDataSourceParam[]Requiredrequire() or URLs. Keep the array stable; a new array reloads.
preset"default" | "soft" | "sharp""default"Shape of the fold: a sharp fold with a small roll, a rounder and broader roll, or a crisp crease.
loopbooleanfalseAfter the last page comes the first.
autoPlaybooleanfalseMove through the pages on a timer. Waits while a finger is down; stops at the last page unless loop is on.
onIndexChange(index: number) => void—Called as each page change starts.
configPartial<RollingCarouselConfig>—Fine tuning. See Advanced.
styleStyleProp<ViewStyle>—Fills its parent unless you set a height or flex.

Advanced

Everything most apps never need to touch.

Every field is optional and overrides the preset.

FieldDefaultDescription
cardsPerView1Cards face-on in the middle. Odd keeps one centred.
viewWidth"72%"Width those cards fill together, in points or a percentage. The rest is folds.
gap16Points between cards.
perspective4Pages stacked past each fold: 2 → one, 4 → two, 6 → three.
foldAngle89Degrees the strip turns at each fold, 0–90.
foldRadius0.25Roundness of the fold in card widths. 0 is a crease.
pageBow0.064How much each page curves back in the middle. 0 is flat.
settleDuration1200Milliseconds a page takes to settle, roughly.
settleDamping0.91 never overshoots; lower bounces.
flickLookahead0.2Seconds of flick speed to look ahead. Higher travels further.
overscrollResistance0.3Drag past the ends. 0 is a wall, 1 is free.
autoPlayInterval3000Milliseconds each page rests.
autoPlayStepDuration1200Milliseconds each step takes.
autoPlayDirection"forward""forward" or "backward".
placeholderColors"auto"Gradient while an image loads, as two colours. Auto follows dark mode.
meshColumns20Smoothness of the page curve. Higher costs more.

MethodDescription
next()One page forward from where it rests or is heading.
previous()One page back.
scrollTo(index, { animated })Settle on a page, or jump there with animated: false. With loop, it takes the shorter way round.

PropDefaultDescription
initialIndex0Starting page. Read once; use the ref to move later.
interactivetrueOff, swiping and tapping do nothing; auto-play, the ref and screen readers still move it.
tapNavigationtrueTap the left or right half to move one page.
accessibilityLabel"Gallery"Read before "3 of 8".
ref—A RollingCarouselHandle. See Ref methods.

  • Vertical swipes pass through to a parent ScrollView, so it works inside scrolling screens.
  • Quick taps queue up: three taps move three pages. onIndexChange fires when each move starts, not when it lands.
  • Cards keep their image's shape and grow as large as they fit. To make them bigger, make the carousel bigger.
  • A spinner shows until the first few pages load (at most 4 seconds), then the carousel fades in, so it never appears half-loaded.

  • Screen readers read it as "Gallery, 3 of 8", and swiping up or down moves one page.
  • With Reduce Motion on, pages move with a short, gentle slide instead of a spring, and auto-play turns off.

How it works

It is one continuous surface, not a row of rotated cards. A mesh is rebuilt every frame in a Reanimated worklet and drawn with Skia, so pages bend smoothly around the fold instead of hinging at a corner. Dragging, the spring and the drawing all run on the UI thread, and nothing redraws while the carousel is still.

Next componentTideline Reveal