About this animation
A loyalty card with physical stamping and a reward-reveal flip, with configurable reward copy, labels, paper color, and fonts.
How it works
onLayoutmeasures the host width. The card is centered, capped at 420 pt wide, and uses a height of 0.78 × its width. Allow that much vertical space; the host owns safe-area padding.- The front is warm ivory paper with a restrained edge gradient, procedural grain, an inset light border, and two soft shadows. The demo uses the repository's fictional Ember & Oat wordmark and offers a complete After hours palette.
- One progress value drives contact (260 ms) and ink spreading (260 ms by default). A noise mask gives the Lucide flame, circular impression, and small droplets uneven ink coverage. The impression compresses slightly and settles at a different small rotation for each slot; no stamp tool obscures the result.
- Contact updates the earned count and
onStampChange. The count fades and rolls into place on the same frame. Rapid taps queue stamps;resetinvalidates pending callbacks and cancels the active sequence. - Stamp ten reveals the reward with a spring-driven turn (
damping: 30,stiffness: 300,mass: 1), a 560 ms highlight inside the card material, and ten restrained flecks. Tap the reward copy to return to the front, or use the separate redemption action. - Autoplay begins with eight stamps, collects visits at 450 and 1,200 ms, and reveals the reward before 3 seconds. It repeats every 5 seconds. The demo uses flow layout, a width cap, and scroll; short containers omit the editorial intro and visit history.
Install
npx expo install @shopify/react-native-skia react-native-reanimated react-native-workletsUse the Reanimated/worklets configuration appropriate to your Expo SDK. Skia on web requires CanvasKit to load before importing the component; use your application's deferred Skia loader. The demo fonts are loaded by the host application, and are not dependencies of the copyable component.
Icons: Lucide (ISC). The flame and scan-line paths are copied from the repository kit; no remote assets are required.
Usage
import { LoyaltyStampCard } from './loyalty-stamp-card';
<LoyaltyStampCard initialStamps={8} style={{ flex: 0, height: 268 }} />;Pass enough height for the card's aspect ratio. At 343 pt wide, the card is approximately 268 pt tall. The host should scroll when enlarged text or long custom copy needs more space.
import { useRef } from 'react';
import { Button, View } from 'react-native';
import { LoyaltyStampCard, type LoyaltyStampCardRef } from './loyalty-stamp-card';
function Membership() {
const card = useRef<LoyaltyStampCardRef>(null);
return (
<View style={{ flex: 1, padding: 24 }}>
<LoyaltyStampCard
ref={card}
initialStamps={7}
clubName="NEIGHBORHOOD CLUB"
accentColor="#9F291D"
highlightColor="#EEA04B"
inkDuration={280}
onStampChange={(count) => saveVisit(count)}
onRedeem={() => openRedemption()}
/>
<Button title="Collect stamp" onPress={() => card.current?.stamp()} />
</View>
);
}saveVisit and openRedemption are application callbacks. The component is a visual interaction; it does not scan QR codes or validate rewards.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| initialStamps | number | 0 | Initial count, clamped to 0–10; use reset for later changes. |
| brandName | string | Coffee club | Printed wordmark. |
| clubName | string | LOYALTY CLUB | Uppercase membership heading. |
| countLabel | string | Visits collected | Prefix for earned count. |
| rewardTitle | string | Your next drink\nis on us. | Two-line reward copy. |
| rewardCode | string | CLUB-0001 | Redemption code. |
| redeemLabel | string | Redeem in store | Reward action label. |
| accentColor | string | #A44F38 | Ink and dark gradient stop. |
| highlightColor | string | #D89669 | Warm gradient stop and particles. |
| paperColor | string | #FAF4E8 | Cream stop of the front surface. |
| surfaceAccentColor | string | #E8D8C1 | Shadow stop of the paper finish. |
| surfaceHighlightColor | string | #FFFDF6 | Light stop of the paper finish. |
| fontFamily | string | System | Button font. |
| titleFontFamily | string | System | Editorial headings. |
| monoFontFamily | string | System | Metadata, rolling count, and code font. |
| inkDuration | number | 260 | Ink spread duration in milliseconds. |
| onStampChange | (count: number) => void | — | Contact callback on the JS thread. |
| onComplete | () => void | — | Called after printing stamp ten. |
| onRedeem | () => void | — | Redemption button callback. |
| accessibilityLabel | string | Add a loyalty stamp | Front action label; count is appended. |
| countSuffix | string | / 10 | Printed earned-count suffix. |
| rewardLabel | string | REWARD UNLOCKED | Reward status heading. |
| returnLabel | string | Return to your stamp card | Reward-copy accessibility label. |
| formatStampCount | (count: number) => string | English earned count | Formats the front accessibility announcement. |
| textColor | string | #382B24 | Front heading and count ink. |
| mutedColor | string | #8B7769 | Front metadata and empty slot outlines. |
| rewardTextColor | string | #FFF8EE | Reward heading and code ink. |
| style | StyleProp<ViewStyle> | — | Outer container style. |
| testID | string | — | Integration identifier. |
Ref methods: stamp(), showFront(), and reset(count = 0). Returning to the front preserves the count. A full card can be tapped to reopen its earned reward. Reset cancels the active sequence and queued taps.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, react-native-worklets 0.13.0, and Skia 2.14.0. Uses the installed Expo Go stack. Supports iOS, Android, and web. No server, recording, or preview is included.
Accessibility
All actions have button roles and labels. The front announces earned stamps. The hidden face is removed from the accessibility tree and cannot receive touches. Every interaction is available by tap; the redemption button has a 48 pt minimum height. Reduced motion replaces the physical stamp with a 180 ms ink fade and the 3D flip with a 200 ms crossfade, removes particles, keeps a static material highlight, and keeps count changes stationary.
Stable control IDs append -stamp, -front, and -redeem to testID. All printed copy and accessibility announcements can be customized. Supply the full palette when changing the paper color.
The demo reserves 60–76 pt at the top for the viewer's close area. It offers Daylight and After hours appearances, uses MM/DD visit dates, and provides a 52 pt collect action and 48 pt appearance selectors. Fixed card copy is intended for short membership names; localize or resize long content in the host application.




