About this animation
A shopping bag with an animated shipping-reward reveal, with configurable products, shipping threshold, accent color, and fonts.
Why this one
Most carts stop at a progress bar. This one lights up the gift, sends a perforated coupon along an arc, and draws a strike through the shipping charge. Removing a product returns the coupon along the same path and returns the gift to rest. The demo uses original Ember & Oat product photography and the catalog's brand kit.
How it works
- Quantities determine the subtotal and eligibility; updates are atomic and callbacks run on the JS thread.
- A continuous
fill = min(subtotal / threshold, 1)drives the gradient and truck with an interruptible spring (default damping 56, stiffness 300, explicit mass 1). The 24-point Lucide truck stops 12 points before the 32-point gift badge, including at 320-point widths. - A real, brief blur stitches the status text transition using a small Android blur target. A reversible reward progress drives a subtle gift pulse, an expanding outline, eight fine particles, coupon arc, shipping strike, and Free label over 560 ms. The coupon travels below the status text on both unlock and removal.
- Numeric columns derive from the animated subtotal. Each column advances only during its own carry, with distance-based fading; continuous cents rest until the final quarter of each step. Values within 0.05 cent of a whole cent snap exactly into place, including spring and floating-point residue. Digits, currency signs, and decimal points share the same line height and font padding.
- The shipping deduction follows the coupon's arrival, so the total rolls down as the reward lands.
- The demo adds the croissant at 1.2 seconds, removes it at 5 seconds, and repeats every 7.6 seconds. The three initial products total $56, leaving $4 to the $60 threshold.
Install
npx expo install @shopify/react-native-skia@2.14.0 expo-blur@~58.0.5 expo-image@~58.0.17 expo-linear-gradient@~58.0.3 react-native-reanimated@4.7.1 react-native-worklets@0.13.0Copy this folder without demo/. Configure Reanimated as recommended for your Expo project. For web, initialize CanvasKit before importing this component, using Skia's LoadSkiaWeb/WithSkiaWeb integration. In this catalog, register it through withSkia; importing Skia before initialization can break the entire page. Fonts are optional and supplied by the host.
Usage
import { FreeShippingUnlock } from './free-shipping-unlock';
<FreeShippingUnlock
products={[
{ id: 'blend', name: 'House blend', price: 18, quantity: 2 },
{ id: 'cup', name: 'Ceramic cup', price: 24, quantity: 0 },
]}
onCheckout={(total) => beginCheckout(total)}
/>;const cart = useRef<FreeShippingUnlockRef>(null);
<FreeShippingUnlock
ref={cart}
products={products}
threshold={60}
shippingCost={6}
colorScheme="auto"
accentColor="#EA580C"
springDamping={26}
springStiffness={300}
fontFamily="YourBodyFont"
titleFontFamily="YourHeadingFont"
monoFontFamily="YourNumericFont"
onUnlockChange={(eligible) => updateShippingEligibility(eligible)}
/>;
// From a host action or timer:
cart.current?.add('cup');Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| products | ShippingProduct[] | [] | Up to four products; initial quantity defaults to 1, 0 marks a recommendation |
| threshold | number | 60 | Shipping unlock amount; minimum 1 |
| shippingCost | number | 6 | Shipping charge below threshold |
| title | string | Your bag | Screen heading |
| brand | ReactNode | — | Host-owned brand content |
| recommendationTitle | string | You might also like | Recommendation heading |
| checkoutLabel | string | Checkout | Checkout button and accessibility label |
| accentColor | string | #EA580C | Reward accent |
| colorScheme | light / dark / auto | light | Surface and text theme |
| fontFamily | string | system | Body typeface |
| titleFontFamily | string | system | Heading typeface |
| monoFontFamily | string | system | Numeric typeface |
| springDamping | number | 56 | Fill and numeric spring damping. Values below the damping needed for ζ ≥ 0.8 are raised to that minimum. |
| springStiffness | number | 300 | Fill and numeric spring stiffness |
| onCartChange | (quantities, subtotal) => void | — | Reports quantity updates |
| onUnlockChange | (unlocked: boolean) => void | — | Reports threshold crossings |
| onBack | () => void | — | Host navigation callback |
| onCheckout | (total: number) => void | — | Host checkout callback |
| accessibilityLabel | string | Shopping bag | Root label |
| style | StyleProp<ViewStyle> | — | Container style |
| testID | string | — | Root test identifier |
ShippingProduct accepts id, name, price, optional image, and optional quantity. Quantities are capped at 99. Numeric windows support totals below $1,000. Provide a stable product list; call reset() to adopt updated initial quantities. The ref also exposes add(id) and remove(id). Checkout remains available during animation and reports the actual cart total.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Skia 2.14.0. Uses modules included in this project's Expo Go stack. Web requires CanvasKit initialization. Supports iOS, Android, and web. The component is self-contained and imports no catalog or theme code. Icons: Lucide (ISC), copied from the catalog kit in icons.ts.
Accessibility
Every action has a button role and product-specific label; stepper and Add targets are at least 44 pt. Eligibility changes use a polite live region; the total has an explicit spoken value and animated digit layers are hidden from screen readers. Reduced motion keeps the fill and numbers static, disables product flight, gift pulse and outline, and particles, and fades the coupon directly into the shipping row over 180 ms. No gesture is required. The header leaves the top-right viewer close control clear.
All springs set mass explicitly to 1. The fill spring enforces a minimum damping ratio of 0.8. Truck and gift use complete Lucide kit paths at the same 24-point size and 2-point stroke; the gift is never split into moving icon fragments.




