About this animation
A review stack with an animated rating summary, with configurable reviews, product image, gradient colors, and fonts.
How it works
- Responsive geometry comes from the component's
onLayout, so the product header and review pile fit a sheet, phone, or tablet. - Each card owns a short, interruptible spring entrance, staggered by 120 ms; a shared impact value gives the card beneath the top card a small landing jolt.
- The summary bars and rolling score use one summary progress value. A separate continuous pile position drives every card on the UI thread: stable review identities stay mounted, the outgoing card clears the screen before changing layers, and then returns beneath the advancing pile. No React reorder or whole-pile fade occurs during cycling.
- Tapping the top card opens a spring sheet. Filters restart the entrance of the matching cards without remounting the card list.
- Lucide paths from the Native Animation kit provide the star, check, close, and brand glyphs without a third-party icon dependency.
Why this one
Most review components are flat lists. This one gives reviews presence: cards fall, shuffle, and return to the back, while the rating summary assembles as part of the same product moment. That makes a familiar commerce pattern feel like a premium, memorable interaction.
Install
npx expo install expo-image expo-linear-gradient @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsUsage
import { ReviewPileSummary } from './ReviewPileSummary';
<ReviewPileSummary productImage={require('./coffee.jpg')} />;<ReviewPileSummary
brandName="Ember & Oat"
brandGradient={['#FB923C', '#B91C1C']}
productTitle="Highland Blend"
productMeta="$18 · 12 oz"
productImage={require('./coffee.jpg')}
fontFamily="Geist_400Regular"
displayFontFamily="InstrumentSerif_400Regular"
monoFontFamily="GeistMono_400Regular"
onFilterChange={(filter) => trackFilter(filter)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
productImage | ImageSourcePropType | undefined | Product image shown in the product header. |
brandName | string | 'Your brand' | Brand name shown beside the product title. |
productTitle | string | 'Coffee blend' | Product title. |
productMeta | string | '$18 · 12 oz' | Product price and size. |
reviews | Review[] | DEFAULT_REVIEWS | Review dataset used to build the pile. |
rating | number | 4.9 | Aggregate rating, from zero to five. |
reviewCount | number | 2184 | Aggregate review count. |
distribution | readonly number[] | [0.87, 0.09, 0.02, 0.01, 0.01] | Distribution from five stars to one star. |
staggerMs | number | 120 | Stagger between landing cards in milliseconds. |
brandGradient | readonly [string, string, ...string[]] | ['#FB923C', '#B91C1C'] | Brand wordmark gradient. |
brandGlyphPath | string | ICON_PATHS.flame | Lucide path used for the brand glyph. |
fontFamily | string | undefined | Body typeface. |
displayFontFamily | string | undefined | Display typeface. |
monoFontFamily | string | undefined | Monospaced typeface used by the rating. |
onFilterChange | (filter: ReviewFilter) => void | undefined | Called when a filter becomes active. |
onReviewCycle | (review: Review) => void | undefined | Called when a review returns to the back of the pile. |
accessibilityLabel | string | 'Review pile summary' | Screen-reader label for the review surface. |
style | StyleProp<ViewStyle> | undefined | Optional style for the outer container. |
testID | string | undefined | Test identifier for the outer container. |
duration | number | 2500 | Summary entrance duration in milliseconds. Spring physics and reduced-motion timing are unchanged. |
The forwarded ref exposes replay(), reset(), next(), expand(), close(), and setFilter(filter). The host app must wrap this component in GestureHandlerRootView and load fonts once. Data changes reset the pile.
Compatibility
Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Skia 2.14.0. Run this component in Expo Go when the host app already includes the listed native modules, or use a development build after adding them to a new project.
Accessibility
The pile has a labeled review surface, 44-point filter and close targets, and button-labeled cards. Tapping the top card opens the full text; the Next review button is the non-gesture alternative to swiping. Screen readers can activate the pile or increment it. Reduced Motion skips the card drop, rotation, and sheet travel while preserving filter, cycling, and expanded-review state changes.
Icons: Lucide (ISC). Skia requires CanvasKit on web; follow the SDK-compatible Skia web setup and load the component only after CanvasKit is ready.
Timeline
Eight cards land from back to front at 120 ms intervals with damping 26 and stiffness 240. Bars and digits settle by 2.2 seconds; the entrance clock ends at 2.5 seconds. The nine-second demo loop then cycles at 3 seconds, expands at 4.3 seconds, closes at 6 seconds, and filters at 6.8 seconds. Filtering collects the pile for 180 ms before the new cards drop. The press spring uses its own value.
Implementation notes
Geometry was reviewed from code, without a browser: 390×844 gives a 327.6×240 card starting at y=455.8; 320×694 gives a 268.8×204 card starting at y=374.8. The eight-card pile adds 28 points of depth. Filters occupy y=384–428 and y=316–360 respectively, leaving space before the pile. The card preview uses 15-point text and three lines; full copy is scrollable in the sheet. Close is on the left; no control sits under the viewer X. Cards, stars, bars, and rolling digits create the first-three-second moment; no opaque block simulates an effect.
Shuffle continuity
All cards remain mounted with a key from review.id (or the unique reviewer name). Relative slots, translations, scale, and layer order come from one shared pile position. At 60% of a cycle the outgoing card is two container widths away, so its layer change happens offscreen; the last 40% tucks it beneath the next card. Spring overshoot is clamped to prevent crossing the slot boundary twice. The next card stays fully opaque throughout. Reduced Motion fades only the outgoing card over 180 ms, leaving the rest of the pile visible. The product image uses expo-image with memory caching and a zero-duration transition.
The continuity logic was checked numerically across complete cycles, both directions, and both target phone widths; compilation does not prove rendered frame continuity.
Customization notes
Use brandGradient for the summary identity.




