About this animation
A photo carousel where one generous card and a row of narrow strips continuously trade space during a drag.
Why this one
Most expanding accordions jump directly between fixed layouts. This version distributes width continuously under the finger, supports either axis, and waits until a card has enough room before sliding and fading its copy into view. Any number of items can be supplied, so the number of visible strips is controlled by the data.
How it works
- The component measures its own container and calculates the active weight required to give the selected strip the requested share of useful space.
- One continuous
positionshared value drives every strip's weight, offset, size, image parallax, copy reveal, and indicator state. - A pan moves that value in proportion to twice the resting narrow-strip size, then velocity and proximity choose the spring target.
- A tap hit-tests the live strip geometry, so selection remains accurate midway through an interrupted transition.
- Copy stays hidden until its strip passes half of its final active size, then enters with opacity and translation.
Install
npx expo install expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsUsage
import ExpandingStripCarousel from './ExpandingStripCarousel';
export function Places() {
return (
<ExpandingStripCarousel
items={[
{ image: require('./forest.jpg'), title: 'Fern Trail', subtitle: 'North Ridge' },
{ image: require('./coast.jpg'), title: 'Quiet Coast', subtitle: 'West Bay' },
]}
/>
);
}Customize its axis and proportions, or select a card from outside:
import { useRef } from 'react';
import ExpandingStripCarousel, { type ExpandingStripCarouselRef } from './ExpandingStripCarousel';
export function VerticalPlaces() {
const ref = useRef<ExpandingStripCarouselRef>(null);
return (
<ExpandingStripCarousel
ref={ref}
items={places}
direction="vertical"
activeShare={0.72}
gap={8}
radius={28}
height={0.7}
fontFamily="YourSansFont"
titleFontFamily="YourDisplayFont"
onIndexChange={(index) => trackSelection(index)}
/>
);
}The forwarded ref exposes select(index).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | StripItem[] | Required | Photos, titles, optional subtitles, and optional accent colors. |
direction | 'horizontal' | 'vertical' | 'horizontal' | Axis along which strips divide the available space. |
activeShare | number | 0.66 | Resting share for the active strip, clamped from 0.5 to 0.85. |
gap | number | 6 | Space between strips in points. |
radius | number | 22 | Strip corner radius in points. |
height | number | 0.42 | Strip-stage height as a fraction of component height. |
fontFamily | string | System font | Font for subtitles. |
titleFontFamily | string | System font | Font for strip and footer titles. |
onIndexChange | (index: number) => void | — | Called when selection changes. |
style | StyleProp<ViewStyle> | — | Outer-container style. |
testID | string | — | Test identifier for the outer container. |
Compatibility
- Expo SDK 57
- React Native 0.86
- Reanimated 4.5 and Worklets 0.10
- iOS: supported, not yet verified on a device
- Android: supported, not yet verified on a device
- Web: supported, pending visual verification
- Works in Expo Go because its native dependencies are included in the SDK
Accessibility
The carousel is exposed as an adjustable control with its active title, place, position, and increment/decrement actions. Tapping a strip provides an alternative to dragging. With reduced motion enabled, taps, accessibility actions, and external selections update immediately without a spring.