Skip to content
Native Animation
Reward Reel Spin animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add reward-reel-spin
Skia

Reward Reel Spin

A three-reel reward reveal with a pull lever and printed coupon, with configurable rewards, labels, accent color, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A three-reel reward reveal with a pull lever and printed coupon, with configurable rewards, labels, accent color, and fonts.

Why this one

Instead of a flat rotating wheel, this pattern feels mechanical: a brass frame holds dark reflective reel windows, the lever stretches against the finger, and each reel settles independently. A traveling wave of 20 bulbs, a luminous winning row, eight jumping coins, and a coupon feeding through a physical slot give the result its own identity. Icons: Lucide (ISC).

How it works

  1. onLayout measures the component. Its frame uses 86% of the available width, while each cream symbol tile stays 56 pt.
  2. A Gesture Handler pan applies progressive elastic resistance after 56 pt. A pull over 35 pt, a lever tap, an accessibility activation, or the separate Spin button triggers the same operation.
  3. One shared millisecond timeline drives all reels and celebrations. Reel travel follows a cubic deceleration; stops occur at 900, 1,300, and 1,700 ms. A 180 ms settling arc adds exactly 6 pt of overshoot.
  4. Two translucent, vertically displaced copies accompany each symbol. Their separation and opacity follow the analytic reel velocity, so the trails disappear as the reel slows down.
  5. Matching symbols light the winning row, accelerate the bulb wave, and launch eight Skia coins. At 1,800 ms the coupon starts feeding downward over 480 ms. onResult runs on the JS thread at 2,350 ms.
  6. The component subtracts accepted spins from spinsLeft. The demo uses an infinite allowance during autoplay: pull at 1 s, miss, win on the second pull at 3.5 s, then restart at 8 s. Touching anywhere in the demo permanently stops that autoplay session.

Install

npx expo install @shopify/react-native-skia expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-worklets

Use GestureHandlerRootView at the app root. In web projects, configure Skia's CanvasKit loading before importing this component; Expo catalog demos should be registered through the existing withSkia loader. No new packages or native configuration are needed in this repository.

Usage

import RewardReelSpin from './reward-reel-spin';

export function DailyReward() {
  return <RewardReelSpin spinsLeft={1} />;
}

Provide a backend-issued outcome and your own content for a real rewards feature:

import { useRef } from 'react';
import RewardReelSpin, { type RewardReelSpinRef } from './reward-reel-spin';

export function RewardScreen() {
  const machine = useRef<RewardReelSpinRef>(null);
  return (
    <RewardReelSpin
      ref={machine}
      spinsLeft={1}
      accentColor="#FBBF24"
      machineWidthRatio={0.9}
      rewards={[{ label: 'A free pastry', symbol: 'croissant', code: 'PASTRY-4K8' }]}
      getOutcome={() => 0}
      onResult={(reward) => saveReward(reward)}
    />
  );
}

saveReward represents your application's result handler. Fetch and authorize the result before allowing a spin. The included random default and client-side allowance are presentation behavior; enforce daily limits and coupon redemption on your backend. spinsLeft is the allowance for this mounted instance, rather than a counter to decrement again in onSpin. Remount the component with a new day/session key to start a new allowance.

Props

PropTypeDefaultPurpose
rewardsreadonly Reward[]DEFAULT_REWARDSReward data. Defaults to three generic coupon rewards.
spinsLeftnumber1Available spins for the current day. The component also subtracts locally consumed spins.
getOutcome() => RewardOutcomeundefinedResolves the result once per spin. Use a backend-issued result for real rewards.
accentColorstring'#F59E0B'Accent for active bulbs, lever, and the winning row.
machineWidthRationumber0.86Machine width as a fraction of its container.
machineHeightRationumber0.45Machine height as a fraction of its container, clamped to 190–254 pt.
spinLabelstring'Spin'Button text.
couponHeadingstring'YOUR REWARD'Coupon heading.
exhaustedLabelstring'Come back tomorrow'Text when no spins remain.
missLabelstring'So close. Thanks for playing.'Text for a nonwinning result.
fontFamilystringundefinedBody typeface; defaults to the system font.
titleFontFamilystringundefinedCoupon headline typeface; defaults to the system font.
monoFontFamilystringundefinedCode typeface; defaults to the system font.
onSpin() => voidundefinedCalled once for each accepted spin.
onResult(reward: Reward | null) => voidundefinedCalled after the result and coupon are revealed.
onInteraction() => voidundefinedCalled when a finger touches the lever or spin button.
accessibilityLabelstring'Pull the lever to spin'Accessible label for the lever.
styleStyleProp<ViewStyle>undefinedOuter container styling.
testIDstringundefinedOuter test identifier.
durationnumber2350Reel spin duration in milliseconds. Spring physics and reduced-motion timing are unchanged.

Reward.symbol supports coffee, croissant, cookie, cup-soda, percent, and gift. Invalid indices and empty reward lists produce a miss.

Ref methods: spin(outcome?) returns whether the operation was accepted. spin(null) forces a miss; spin(0) selects the first reward. reset() clears an idle result without replenishing the allowance. Repeated triggers while the reels are active are ignored, while the lever remains movable and interruptible.

Compatibility

Short containers scroll vertically when the coupon and controls need more space. The empty coupon window collapses in this layout; the full coupon remains available after a win.

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. Intended for iOS, Android, and web, including the matching Expo Go build. Keep at least 310 pt of width and approximately 415 pt of height for the complete machine, coupon, status, and button.

Accessibility

The lever exposes a button role, action, label, hint, and disabled state. Its 46×48 pt touch target supports drag and tap through Gesture.Exclusive; the separate button is 52 pt tall. Important spin and result changes use a polite live region and native accessibility announcements. The coupon is hidden from the accessibility tree until it is revealed.

Reduced motion keeps the staggered results but replaces symbol changes with 200 ms fades, the coupon with a 200 ms fade, and the lever with a static control. It removes rolling, trails, rebounding, coin flights, and bulb waves. Interface pressure uses a separate shared value and never cancels the reel timeline.

Art direction and review notes

The demo uses the kit's Ember & Oat flame tile, warm ember mesh, cream tiles, and one amber accent. Its brand occupies the upper left; the upper-right viewer close area is clear. Geometry is tuned for 390×844 and 320×694 with a two-line serif headline and a separate lower prize menu. Velocity trails and traveling light are visible within the first three seconds; the second-attempt coupon win follows at approximately five seconds. Glows are blurred strokes or radial gradients, and shading stays inside the reel windows; no rectangular highlight covers text.