About this animation
A physics-driven decision deck with gesture-reactive stamps, accessible controls, and undo.
How it works
- The component measures its container and sizes the cards, fan, stamps, and controls proportionally.
- One horizontal shared value drives the active card, decision tint, stamp reveal, and the progress that advances every card behind it.
- Release distance and horizontal velocity jointly decide whether the card returns or exits; the exit spring preserves the gesture velocity.
- The initial touch height reverses the rotation direction, so the card turns naturally around the point where it was grabbed.
- Completed decisions are kept in a small history stack.
undo()remounts the previous card on the side where it exited and springs it home.
Why this one
Most swipe decks stop at a card flying left or right. This version communicates the decision while the finger moves, prepares the next card continuously, accounts for throw velocity and grip position, and includes both undo and a complete touch-accessible alternative to the gesture.
Install
npx expo install react-native-gesture-handler react-native-reanimated react-native-workletsUsage
import DecisionSwipeDeck from './DecisionSwipeDeck';
export function Choices() {
return (
<DecisionSwipeDeck
items={options}
keyExtractor={(item) => item.id}
renderCard={(item) => <OptionCard option={item} />}
onSwipe={(item, direction) => saveDecision(item.id, direction)}
/>
);
}Customize labels, the threshold, visible stack, and imperative controls:
import { useRef } from 'react';
import DecisionSwipeDeck, { type DecisionSwipeDeckRef } from './DecisionSwipeDeck';
export function EventPicker() {
const deckRef = useRef<DecisionSwipeDeckRef>(null);
return (
<DecisionSwipeDeck
ref={deckRef}
items={events}
keyExtractor={(event) => event.id}
renderCard={(event) => <EventPoster event={event} />}
yesLabel="SAVE"
noLabel="PASS"
threshold={0.24}
visibleCount={4}
fontFamily="YourSansFont"
onEmpty={() => showSummary()}
/>
);
}The forwarded ref exposes swipe('left' | 'right') and undo().
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | Required | Items shown in deck order. |
renderCard | (item: T, index: number) => ReactNode | Required | Renders the contents of a card. |
keyExtractor | (item: T) => string | Required | Returns a stable key for an item. |
yesLabel | string | 'YES' | Positive stamp and button label. |
noLabel | string | 'NOPE' | Negative stamp and button label. |
threshold | number | 0.28 | Release threshold as a fraction of container width. |
visibleCount | number | 3 | Maximum number of mounted cards. |
onSwipe | (item: T, direction: 'left' | 'right') => void | — | Called after a card exits. |
onEmpty | () => void | — | Called when the last card exits. |
showButtons | boolean | true | Shows the No, Undo, and Yes controls. |
fontFamily | string | System font | Font used by decision stamps. |
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, not yet verified
- Works in Expo Go because its native dependencies are included in the SDK
Accessibility
The three circular controls are labeled buttons and provide a full alternative to dragging. The active card is exposed as an adjustable decision card with a gesture hint. When reduced motion is enabled, decisions and undo update immediately without flying or spring movement.