About this animation
A currency converter with crossing cards and rolling amounts, with configurable rates, initial amount, accent color, and fonts.
Why this one
A typical swap replaces two labels instantly. Here, the cards exchange places along opposite arcs: the front card grows to 1.04 while the rear card recedes to 0.96. The button rotates, both amounts roll to a fresh calculation, and the seven-day chart redraws with its direction inverted. A continuous lock ring makes the quote's remaining lifetime visible.
How it works
- Two stable card identities share one continuous swap position. Vertical displacement, lateral arcs, scale, button rotation, and chart inversion all derive from that value.
- Tap swaps the pair; dragging the center control projects velocity to either endpoint. A new gesture can interrupt the spring. Swapping keeps the entered source amount and recalculates the destination.
- Each amount is converted to integer minor units. Digit columns advance only during their own carry and stop exactly on an integer; numeric interpolation runs on the UI thread.
- A spring reveals the five-currency glass selector. Selecting the other card's currency exchanges the pair, preventing duplicate codes. JPY uses whole units.
- A shared lock value drains continuously. Text updates once per second. Expiration flashes the rate, requests a new snapshot, and animates the recalculated amount. Without a callback, small deterministic rate changes illustrate the interaction.
- The demo types 500, swaps at 1.5 seconds, selects JPY, and swaps again. Direct touch cancels the loop and every pending step.
Install
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler expo-blur expo-linear-gradient @shopify/react-native-skiaWrap your app in GestureHandlerRootView. For web, load CanvasKit before mounting this component with Skia's WithSkiaWeb or your app's existing lazy Skia loader. The demo must be registered with the catalog's withSkia loader. No registration is included in this folder.
Icons: Lucide (ISC). The copied paths are in icons.ts. The Aster demo mark uses the kit's sparkle glyph and violet tile.
Usage
import CurrencyConverterSwap from './currency-converter-swap';
<CurrencyConverterSwap initialAmount={500} onSend={quote => reviewTransfer(quote)} />With custom rates and personality:
import { useRef } from 'react';
import { CurrencyConverterSwap, type CurrencyConverterSwapRef } from './currency-converter-swap';
const converter = useRef<CurrencyConverterSwapRef>(null);
<CurrencyConverterSwap
ref={converter}
initialFrom="GBP"
initialTo="JPY"
initialAmount={250}
accentColor="#34D399"
colorScheme="dark"
fontFamily="YourInterfaceFont"
monoFontFamily="YourMonoFont"
lockSeconds={45}
springDamping={28}
rates={{ USD: 1, EUR: 0.9214, JPY: 149.72, GBP: 0.7682, MXN: 19.43 }}
onRefreshRates={() => cachedRates}
onChange={quote => updateQuote(quote)}
/>
// Invoke from an event handler:
converter.current?.swap();The component does not load fonts. Load your fonts once in the host; the included demo uses Geist, Geist Mono, and Instrument Serif.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
initialFrom | CurrencyCode | USD | Initial source code. |
initialTo | CurrencyCode | EUR | Initial destination code. |
initialAmount | number | 500 | Initial amount, clamped to 0–999999. |
rates | Record<CurrencyCode, number> | Sample snapshot | Positive reference units per USD, read initially and on reset. |
onRefreshRates | () => Record<CurrencyCode, number> | undefined | — | Returns a synchronous cached snapshot on expiry. |
lockSeconds | number | 45 | Lock duration, minimum 5 seconds. |
accentColor | string | #34D399 | Six-digit hex accent, including derived translucent tints. |
colorScheme | 'light' | 'dark' | 'auto' | dark | Surface palette. |
fontFamily | string | System | Interface font. |
monoFontFamily | string | System | Amount and rate font. |
springDamping | number | 26 | Crossing spring damping, minimum 26. |
onChange | (quote: Conversion) => void | — | Event-level quote updates on JS. |
onSend | (quote: Conversion) => void | — | Send intent; the host performs the transfer flow. |
onInteraction | () => void | — | Direct touch or accessible activation. |
accessibilityLabel | string | Currency converter | Root label. |
style | StyleProp<ViewStyle> | — | Root style; defaults to flex: 1. |
testID | string | — | Root identifier. |
CurrencyCode is USD, EUR, JPY, GBP, or MXN. Conversion contains from, to, amount, convertedAmount, and rate. Destination amounts are capped at 9999999. The sample snapshot is illustrative; a production host supplies validated quotes, fees, and final transfer confirmation.
Ref methods: swap(), setAmount(number), typeKey(string), selectCurrency('send' | 'receive', code), openCurrencyPicker(side), and reset(). typeKey accepts digits, ., or delete.
Compatibility
Short containers scroll vertically through the quote, rate, keypad, and send action. Each section reserves its full height so the keypad cannot overlap the rate.
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. Uses installed modules compatible with Expo Go. Android blur uses one BlurTargetView, with the SDK 31+ blur method and translucent fallback on older devices.
Supports iOS, Android, and web.
Accessibility
Every action has a button role and descriptive label, with targets at least 44 points. The swap supports tap, vertical drag, and screen-reader activation. Currency changes, swaps, refreshed rates, and send intent produce announcements; changing status uses a polite live region. Digit columns are hidden from accessibility and expose a single formatted amount.
Reduced motion removes card flights, scaling, rotating controls, and background blur. Changes use 200 ms opacity transitions, numbers update without rolling, and the sparkline does not redraw progressively. The currency sheet remains dismissible with its Cancel button or backdrop.




