About this animation
A premium subscription paywall where the mascot responds to plan choices and celebrates purchase intent.
Why this one
Most mascot paywalls treat the character as decoration. Here, the mascot breathes and blinks at rest while Lucide stars orbit the hero. Plan selection changes its brows, cheeks, tilt, and pose with an interruptible spring. Pressing the CTA triggers a focused 520 ms jump, stretching shadow, halo, and particle burst in the selected plan's accent, making the conversion moment feel earned without delaying checkout.
How it works
onLayoutmeasures the component so the mascot, typography, cards, and celebration scale inside any parent instead of depending on the window.- A continuous
selectionshared value connects each plan card to the mascot's pose and expression; tapping another card springs every dependent property from its current value. - The selected plan owns the single accent used by the hero, controls, checks, CTA glow, mascot cheeks, and celebration particles.
pressCta()runs a short UI-thread sequence: the mascot jumps, its shadow stretches, a halo expands, and ten particles travel outward before fading.- The component keeps checkout behavior on the JS thread through callbacks, while the forwarded ref gives demos and host apps deterministic control.
Install
npx expo install @expo/vector-icons expo-image expo-linear-gradient react-native-reanimatedUsage
import MascotPaywall from './MascotPaywall';
export function UpgradeScreen() {
return (
<MascotPaywall
mascot={require('./assets/mascot.png')}
onCtaPress={() => openCheckout()}
/>
);
}Customize the plans, copy, fonts, and accent while keeping the same interaction model:
import { useRef } from 'react';
import MascotPaywall, { type MascotPaywallRef } from './MascotPaywall';
export function UpgradeScreen() {
const paywallRef = useRef<MascotPaywallRef>(null);
return (
<MascotPaywall
ref={paywallRef}
mascot={require('./assets/mascot.png')}
brand="Daymark"
tier="Plus"
headline={['Build a ', 'brighter', ' learning habit']}
accentColor="#6E56FF"
plans={[
{
badge: 'Flexible',
title: 'Monthly',
subtitle: 'Billed monthly',
price: '$7.99',
period: '/ month',
},
{
badge: 'Save 58%',
title: 'Yearly',
subtitle: 'Billed $39.99 yearly',
price: '$3.33',
period: '/ month',
},
]}
onPlanChange={(index, selectedPlan) =>
trackPlan(index, selectedPlan.title)
}
onCtaPress={() => openCheckout()}
/>
);
}The forwarded ref exposes selectPlan(index), pressCta(), and celebrate().
Icons: Lucide/Feather (ISC).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
mascot | ImageSourcePropType | Required | Transparent mascot image displayed in the hero. |
brand | string | 'Brightpath' | Product name. |
tier | string | 'Plus' | Paid tier label. |
headline | [string, string, string] | ['Grow a little ', 'brighter', ' every day'] | Headline before, inside, and after the accented segment. |
plan | PaywallPlan | Monthly offer | Backward-compatible first plan when plans is omitted. |
plans | PaywallPlan[] | Monthly and Yearly | Selectable subscription plans. |
initialPlanIndex | number | 1 | Initially selected plan, clamped to the available range. |
features | PaywallFeature[] | Three learning benefits | Benefits shown in the premium surface. |
ctaLabel | string | 'Start 7 days free' | Main action label. |
note | string | 'No charge today · Cancel anytime' | Reassurance below the CTA. |
moreLabel | string | 'Choose a plan' | Accessible label for the plan group. |
accentColor | string | '#6E56FF' | Default accent for selection, checks, glow, and particles. |
topColors | [string, string] | Accent-derived | Optional hero gradient override. |
sparkColor | string | Selected accent | Optional mascot cheek and sparkle color. |
fontFamily | string | System font | Interface and supporting-text family. |
titleFontFamily | string | System font | Headline, plan-title, and price family. |
onCtaPress | () => void | — | Called after the CTA celebration starts. |
onClose | () => void | — | Called when Close is pressed. |
onMorePress | () => void | — | Legacy callback called when a plan is selected. |
onPlanChange | (index, plan) => void | — | Called with the newly selected plan. |
style | StyleProp<ViewStyle> | — | Outer-container style. |
testID | string | — | Test identifier for the outer container. |
PaywallPlan accepts badge, title, subtitle, strikePrice, price, period, and an optional accentColor.
Compatibility
- Expo SDK 57
- React Native 0.86
- Reanimated 4.5
- iOS: supported, not yet verified on a device
- Android: supported, not yet verified on a device
- Web: supported, not yet visually verified
- Works in Expo Go because the required native modules are included in the SDK
Accessibility
The close and CTA controls expose button roles and labels. Plans are presented as a labeled radio group, and every plan reports its selected state and price. Reduce Motion replaces spring-driven reactions and travel with short fades while keeping plan selection and the CTA fully functional.




