About this animation
A payment sheet with a sliding card stack and card-to-receipt finish, with configurable cards, labels, accent color, and fonts.
Why this one
An ordinary Pay button skips the feeling of deliberate confirmation. This sheet connects a double tap to a side-button indicator beside the card, away from the sheet header and the viewer close control. Switching uses an interruptible lateral stack with dark material peeking out by 8 pt and a fading ‘Everyday · Debit’ caption. This reads as choosing an account immediately. Labeled 48 pt controls also avoid asking users to tap a narrow card edge. A continuous ring frames authorization; the confirmation flip reveals a real magnetic stripe, signature panel, masked CVV, and wallet wordmark before the sheet contracts into a receipt. The receipt uses the sheet's actual dimensions rather than a cover placed over it.
How it works
- Measure the container with
onLayout; sheet, card, and side-button placement follow its dimensions. - One UI-thread progress value drives opening, authorization, card flip, contraction, and receipt departure.
- A continuous card position drives the interruptible lateral stack and caption opacity independently of payment progress. Both confirmation faces rotate individually with a constant 900 pt perspective, hidden backfaces, and angle-based visibility guards; mirrored text never shows.
- Gesture Handler recognizes a deliberate double tap; a labeled single-tap alternative triggers the same finish. Drag the title to dismiss, or tap Cancel.
- Skia trims a rounded rectangular path around the card over 800 ms. Lucide supplies all icon paths.
- Emit state callbacks and accessibility announcements at milestones. The demo owns and cleans up its autoplay timers; user input cancels the loop.
Install
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler expo-blur expo-linear-gradient @shopify/react-native-skiaWrap your application in GestureHandlerRootView. For web, configure Skia's CanvasKit loading as required by your Expo application. No external images, fonts, or network assets are needed by the component. Icons: Lucide (ISC).
Usage
import { useRef } from 'react';
import ExpressPaySheet, { type ExpressPaySheetRef } from './express-pay-sheet';
function Checkout() {
const payment = useRef<ExpressPaySheetRef>(null);
return (
<ExpressPaySheet
ref={payment}
merchant="Your store"
amount="$42.50"
/>
);
}Call payment.current?.open() from your checkout button, or use the built-in Open payment sheet action.
<ExpressPaySheet
merchant="Your store"
walletName="Wallet"
shipping="Home · Standard delivery"
contact="customer@example.com"
accentColor="#A78BFA"
confirmationDuration={900}
springDamping={30}
cards={[
{ label: 'Everyday', lastFour: '4821', colors: ['#6850B5', '#30264D'] },
{ label: 'Reserve', lastFour: '9064', colors: ['#39335F', '#8B7CBA'] },
]}
/>This is a visual interaction, not a payment SDK, passcode validator, or listener for the physical side button. Connect your production authentication and payment flow separately. onPaid reports visual success.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Background app content. |
| merchant | string | Your store | Receipt merchant. |
| amount | string | $42.50 | Formatted amount. |
| shipping | string | Home · Standard delivery | Shipping summary. |
| contact | string | customer@example.com | Contact summary. |
| walletName | string | Wallet | Wallet heading and card identity. |
| cards | readonly PaymentCard[] | Three generic cards | Up to three labels, optional type (default Debit), last-four digits, and gradients; empty lists use defaults. |
| fontFamily | string | System | UI typeface. |
| monoFontFamily | string | System | Price and account digits. |
| accentColor | string | #22D3EE | Ring and success check. |
| surfaceColor | string | #0B0B12 | Glass sheet tint. |
| confirmationDuration | number | 800 | Ring duration in ms, minimum 200. |
| springDamping | number | 28 | Sheet damping, minimum 28 with explicit mass 1 (ζ ≥ 0.86). |
| confirmationLabel | string | Double-click to pay | Visible and accessible confirmation label. |
| alternativeLabel | string | Pay with passcode | Single-tap alternative. |
| securityCodeLabel | string | CVV ••• | Masked code on the real reverse; never provide a real security code. |
| onPaid | () => void | — | Visual success callback. |
| onStateChange | (state: PaymentState) => void | — | closed, ready, authorizing, paid. |
| onCardChange | (index: number) => void | — | Selected card index. |
| onInteraction | () => void | — | Stops demo autoplay on user input. |
| style | StyleProp<ViewStyle> | — | Root styling; defaults to flex: 1. |
| testID | string | — | Root identifier, also used for -confirm, -cancel, -pay, -card-N, and -select-N controls. |
Ref methods: open(), close(), reset(), expandCards(), selectCard(index), pay(). Close cancels payment animations. Selection is interruptible before authorization; duplicate payment requests are ignored. Reopening starts a fresh visual checkout.
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses installed Expo Go modules. Android blur uses BlurTargetView and dimezisBlurViewSdk31Plus; older Android versions use the native fallback. Supports iOS, Android, and web.
Accessibility
Buttons have roles, labels, 48 pt minimum targets, and selection states. Card-choice buttons provide an explicit touch alternative to the stack. Important transitions announce to assistive technology; the receipt has a polite live region. A single tap on Pay with passcode replaces the double tap. Screen readers can activate the side indicator directly. Cancel replaces dragging to dismiss. Reduced motion removes blur, pulsing, card travel, flips, ring animation, and receipt flight; 200 ms opacity transitions reveal the result with a discrete layout change.
Demo timing
Open at 0.1 s, fan at 0.75 s, select at 1.15 s, confirm at 1.7 s, ring completes at 2.5 s, the real reverse arrives at 2.94 s. Subsequent cycles alternate Everyday and Reserve. The sheet then contracts, holds the receipt, fades, and repeats after reset. Every user touch stops autoplay.
Layout
The host supplies safe-area insets. Card width is capped at 340 pt and leaves space for the side indicator; sheet height reserves the top 72 pt for the catalog viewer. Content scrolls in short containers and landscape rather than shrinking text or hiding payment actions. At 320×694 and 390×844 the checkout content fits in the sheet without scrolling at default text size. The reverse is physical card material, not a success overlay or a block sweeping over text.





