A fast, secure SVGA player for React Native, built on Nitro Modules. Streams .svga files from the network or disk, caches them transparently, and plays both built-in audio tracks and external MP3s without disrupting the host app's audio session.
SVGA is a vector + sprite animation format used widely in livestream gift animations. It's lighter than Lottie for raster-driven animations and supports frame-synced audio.
Expo users: this library ships native code, so it cannot run inside Expo Go. You'll need a development build (via
expo prebuildor EAS Build). Full instructions below.
- Plays every common SVGA packaging - v2 zip (
PK…), zlib stream (78 xx), gzip stream (1F 8B), or raw protobuf - Stream from anywhere - remote URLs, local files (
file://), or bundled assets (asset://) - Two-tier cache - SHA-256-keyed disk cache + in-memory LRU of decoded entities, so the second play of a recent file is instant
- Built-in + external audio - plays
.svga-bundled audio tracks; can layer external sounds that play onstart/finish. Externals auto-mute the built-in audio when present (override available) - Broad audio format support - Android plays MP3 / AAC / M4A / OGG / WAV / FLAC / AMR via
MediaPlayer; iOS plays MP3 / AAC / M4A / WAV / AIFF / ALAC viaAVAudioPlayer - Doesn't fight other audio modules - uses isolated
MediaPlayerinstances (Android) and per-trackAVAudioPlayerinstances (iOS) without touchingAVAudioSession - Programmatic controls -
play,pause,stop,seekToFrame,seekToProgressvia a ref - Lifecycle events -
onStart,onLoop,onFinish,onError - Transparent background - no card-coloured rectangle; restyle freely
- Hardened parser - bounded varints, size caps, zip-slip rejection, scheme allow-list
- Drift-corrected frame loop with no per-frame allocation
- TypeScript first
- React Native ≥ 0.74
- iOS 13+ / Android
minSdkVersion24 react-native-nitro-modules≥ 0.29
This package autolinks. There's no manifest to edit, no linking command to run; React Native CLI and Expo prebuild both pick it up automatically. The flow differs slightly depending on your project type.
npm install @dasimems/react-native-svga react-native-nitro-modules
# or
yarn add @dasimems/react-native-svga react-native-nitro-modulesThen install the iOS pods:
cd ios && pod install && cd ..Rebuild the native projects (the JS dev server will not pick up new native code on its own):
npx react-native run-ios
# or
npx react-native run-android
@dasimems/react-native-svgadoes not work in Expo Go. Expo Go is a fixed, prebuilt binary - it cannot load custom native modules. You must use a development build (expo prebuild+ a custom client). This is a one-time setup; afterwards your dev workflow is the same as with Expo Go.
npx expo install @dasimems/react-native-svga react-native-nitro-modules(Using expo install instead of npm install makes Expo align the versions with your SDK.)
npx expo prebuildThis creates the ios/ and android/ folders in your app, runs autolinking, and wires up @dasimems/react-native-svga (and react-native-nitro-modules) automatically. You only need to re-run prebuild when you add or remove a native module.
The first time:
# locally
npx expo run:ios
# or
npx expo run:androidOr via EAS Build:
eas build --profile development --platform ios
eas build --profile development --platform androidAfter the development client is installed on your device/simulator, your daily workflow goes back to:
npx expo start --dev-client- Stuck on Expo Go? If
expo startopens the project in Expo Go, hitsin the terminal to switch to "development build" mode, or runnpx expo start --dev-client. - Updated Expo SDK? Re-run
npx expo prebuild --cleanto regenerate the native projects from scratch. - Custom config plugins? Move any patches into
app.config.jsconfig plugins; raw edits toios//android/are wiped by the next prebuild.
After installing, confirm the native module is registered. From your app's entry file:
import { SvgaCache } from '@dasimems/react-native-svga';
console.log(await SvgaCache.size()); // should print a number, not throwIf it throws "module not found", the autolinking step didn't run - rebuild the app (pod install + native rebuild for bare; expo prebuild + run for Expo).
import { SvgaPlayer } from '@dasimems/react-native-svga';
import { View, StyleSheet } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<SvgaPlayer
source="https://assets.svga.io/svga/sample.svga"
style={styles.player}
loops={0}
onFinish={() => console.log('finished')}
onError={(message) => console.warn('svga error:', message)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
player: { width: 300, height: 300 },
});SvgaPlayer autoplays by default and renders with a transparent background, so you can drop it onto any screen.
The source prop accepts three URI styles:
| Form | Example | Notes |
|---|---|---|
| Remote URL | https://cdn.example/anim.svga |
Downloaded once, cached on disk, replayed from memory |
| Local file | file:///var/.../anim.svga or /var/.../anim.svga |
Read directly, bypassing the cache |
| Bundled asset | asset://animations/cheer.svga |
Read from the platform asset bundle |
Schemes other than http, https, file, and asset are rejected for safety, as are paths containing ...
// Remote
<SvgaPlayer source="https://cdn.example/gift.svga" />
// Local file (e.g. one you saved with expo-file-system)
<SvgaPlayer source="file:///path/to/local/anim.svga" />
// Bundled asset
// Android: place file under android/app/src/main/assets/animations/cheer.svga
// iOS: add to the Xcode bundle as animations/cheer.svga
<SvgaPlayer source="asset://animations/cheer.svga" />SvgaPlayer forwards a ref that exposes imperative methods:
import { useRef } from 'react';
import { Button, View } from 'react-native';
import { SvgaPlayer, type SvgaPlayerHandle } from '@dasimems/react-native-svga';
export function Controlled() {
const ref = useRef<SvgaPlayerHandle>(null);
return (
<View>
<SvgaPlayer
ref={ref}
source="https://cdn.example/gift.svga"
autoPlay={false}
loops={3}
/>
<Button title="Play" onPress={() => ref.current?.play()} />
<Button title="Pause" onPress={() => ref.current?.pause()} />
<Button title="Stop" onPress={() => ref.current?.stop()} />
<Button title="Skip to halfway"
onPress={() => ref.current?.seekToProgress(0.5)} />
</View>
);
}Available methods on the handle:
| Method | Effect |
|---|---|
play() |
Starts (or resumes) playback. Fires onStart on a fresh start. |
pause() |
Suspends playback at the current frame. |
stop() |
Resets to frame 0 and clears any active audio streams. |
seekToFrame(frame) |
Jumps to a specific frame index. |
seekToProgress(progress) |
Jumps to 0.0-1.0 of the timeline. |
Layer your own sounds on top of the animation and decide when each one fires:
import { SvgaPlayer, type SvgaSound } from '@dasimems/react-native-svga';
const sounds: SvgaSound[] = [
{ key: 'open', url: 'https://cdn.example/open.mp3', playOn: 'start', volume: 0.8 },
{ key: 'cheer', url: 'https://cdn.example/cheer.mp3', playOn: 'finish' },
];
<SvgaPlayer
source="https://cdn.example/gift.svga"
sounds={sounds}
loops={1}
/>;Behaviour:
- Sounds are downloaded the first time they're seen and cached on disk by
key. - When the array is non-empty, the built-in audio bundled inside the
.svgais muted automatically. PassmuteBuiltInAudio={false}to layer both. - Sounds unload automatically when the component unmounts (or when
soundschanges). - The library uses an isolated audio pool, so it won't interrupt music or other audio modules in your app.
<SvgaPlayer
source={url}
loops={2}
onStart={() => analytics.track('svga_play_started')}
onLoop={(count) => console.log(`finished loop ${count}`)}
onFinish={() => analytics.track('svga_play_finished')}
onError={(msg) => Sentry.captureMessage(`svga: ${msg}`)}
/>onStart fires on the transition from idle to playing. onLoop fires every time a loop completes. onFinish fires once when the configured loops count is reached. onError fires for any load, parse, or network failure.
For animations you know you'll play (e.g. gift animations in a livestream), warm the cache at app startup so the first play is instant:
import { SvgaCache } from '@dasimems/react-native-svga';
useEffect(() => {
SvgaCache.preload([
'https://cdn.example/gift-1.svga',
'https://cdn.example/gift-2.svga',
'https://cdn.example/gift-3.svga',
]).catch((e) => console.warn('preload failed', e));
}, []);Preload is idempotent — items already on disk under the same cacheKey are skipped, no network request issued. Calling preload(...) repeatedly with the same input is therefore cheap; only entries missing from disk (or expired via setMaxAgeMs) are downloaded. If you bump a cacheKey, only that one entry re-downloads.
The full facade:
// Preload accepts plain URLs or { url, cacheKey } items. Use `cacheKey` to
// decouple cache identity from the download URL — bumping the key forces a
// fresh download and stores the new bytes under the new key, leaving any
// in-flight player on the old key untouched until it releases.
SvgaCache.preload([
'https://cdn.example/gift-1.svga', // url-keyed
{ url: 'https://cdn.example/gift-2.svga', cacheKey: 'g2-v3' },// versioned
]);
SvgaCache.has(url); // boolean — disk hit?
SvgaCache.has(url, cacheKey); // boolean — disk hit under cacheKey?
SvgaCache.path(url); // string | undefined — disk path
SvgaCache.path(url, cacheKey); // string | undefined — disk path under cacheKey
await SvgaCache.size(); // total bytes on disk
await SvgaCache.count(); // total entries on disk
SvgaCache.setLimit(50 * 1024 * 1024); // disk bytes; LRU evicts to fit
SvgaCache.setMemoryLimit(16 * 1024 * 1024); // in-memory decoded-entity bytes
SvgaCache.setMaxAgeMs(24 * 60 * 60 * 1000); // TTL — stale entries miss + re-download
await SvgaCache.evictExpired(); // sweep TTL-expired entries; returns count removed
SvgaCache.clear(); // wipes both disk and in-memory cachesThe disk cache lives under the platform cache directory (Context.cacheDir/svga_cache on Android, Caches/svga_cache on iOS). Files are SHA-256-keyed by cacheKey (which falls back to the URL when omitted), so the same key maps to the same file across launches and devices. When a write would exceed setLimit, the LRU policy evicts the least-recently-used entries first; entries older than setMaxAgeMs are treated as misses on read and reaped by evictExpired().
The <SvgaPlayer> component takes the same cacheKey prop:
<SvgaPlayer
source="https://cdn.example/gift-2.svga"
cacheKey="g2-v3"
loops={2}
/>Bumping cacheKey triggers a fresh download under the new key while still serving the URL.
The view has a transparent background; it inherits whatever style you give it. Common patterns:
// Fixed size
<SvgaPlayer source={url} style={{ width: 200, height: 200 }} />
// Fill a container, contain the content
<SvgaPlayer source={url} style={StyleSheet.absoluteFill} scaleMode="aspectFit" />
// Cover the container, cropping if needed
<SvgaPlayer source={url} style={styles.cover} scaleMode="aspectFill" />
// Stretch to the container regardless of aspect ratio
<SvgaPlayer source={url} style={styles.stretch} scaleMode="fill" />Render conditionally and let key reset the player between sources:
const [step, setStep] = useState(0);
const sources = [intro, body, outro];
<SvgaPlayer
key={step}
source={sources[step]}
loops={1}
onFinish={() => setStep((s) => Math.min(s + 1, sources.length - 1))}
/>;| Prop | Type | Default | Description |
|---|---|---|---|
source |
string |
- | URL, file path, or bundled asset |
cacheKey |
string |
source |
Identity used for cache lookup/storage. Defaults to source. Bump it (e.g. "gift-2.v3") to force a fresh download for the same URL — the new bytes are stored under the new key, while any in-flight player still on the old key keeps playing it until released. |
loops |
number |
0 |
Number of loops; 0 = infinite |
autoPlay |
boolean |
true |
Play immediately once loaded |
speed |
number |
1.0 |
Playback speed multiplier (0.05 minimum) |
muteBuiltInAudio |
boolean |
auto | If omitted, becomes true when sounds is non-empty |
builtInAudioVolume |
number |
1.0 |
0-1 |
playInBackground |
boolean |
false |
If false, playback (frames + audio) auto-pauses when the player leaves the visible window or the app is backgrounded. Set to true for video-style background audio. |
sounds |
SvgaSound[] |
- | Extra MP3s to play alongside the animation |
scaleMode |
'fill' | 'aspectFit' | 'aspectFill' |
'aspectFit' |
How the content fits the view |
style |
ViewStyle |
- | Standard RN view style |
onStart |
() => void |
- | Fires when playback transitions from idle to playing |
onLoop |
(count: number) => void |
- | Fires at the end of each loop |
onFinish |
() => void |
- | Fires when loops is reached |
onError |
(message: string) => void |
- | Fires on load / parse / network errors |
type ScaleMode = 'fill' | 'aspectFit' | 'aspectFill';
interface SvgaSound {
key: string;
url: string;
playOn: 'start' | 'finish';
volume?: number; // 0..1
}
interface SvgaPlayerHandle {
play: () => void;
pause: () => void;
stop: () => void;
seekToFrame: (frame: number) => void;
seekToProgress: (progress: number) => void;
}- Renders into a custom
ViewviaCanvas+drawBitmap, transparent by default - Uses
MediaPlayer(per track) withUSAGE_MEDIAso it doesn't request audio focus; broad codec coverage - Network requests use
HttpURLConnectionwith explicit 15s connect / 30s read timeouts - Bundle and per-entry size limits guard against malicious zips (64 MB bundle / 32 MB entry)
- Renders into a
UIViewdriven byCADisplayLink(weak proxy, no retain cycle) - Audio uses
AVAudioPlayerinstances and deliberately does not configureAVAudioSession, so the host app's session category is preserved - ZIP decompression uses Apple's
Compressionframework (raw DEFLATE) - no third-party dependency URLSessionrequests use 15s/60s timeouts
- Decoded
.svgaentities are kept in an LRU keyed by source string. The default limit is 32 MB on regular devices, automatically reduced to 8-16 MB on devices flagged as low-RAM (ActivityManager.isLowRamDeviceormemoryClass < 256). On iOS,NSCacheuses the same default and auto-purges under memory pressure. - On Android, the memory cache subscribes to
ComponentCallbacks2. When the system reportsTRIM_MEMORY_RUNNING_LOWthe cache halves itself;TRIM_MEMORY_RUNNING_CRITICAL(oronLowMemory) clears it. This keeps long sessions stable on small devices without manual intervention. - Per-frame transforms are pre-composed at parse time, so the draw path is just
concatenate(matrix) + draw- no per-frame allocation. - Bitmaps are decoded with a 2048 px longest-side cap (
BitmapFactory.inSampleSizeon Android,kCGImageSourceThumbnailMaxPixelSizeon iOS) so a single oversized asset can't OOM the device. - Concurrent loads of the same URL are coalesced into one network + parse + decode.
- Disk cache evicts the oldest files (by mtime) when over the configured byte limit.
- The frame loop tracks expected wake-up time and self-corrects so playback doesn't drift on a busy main thread.
- URL scheme allow-list (
http,https,file,asset) - Path traversal (
..) rejected at the validator and in zip entry names - Bundle and entry size caps in the parser
- Atomic disk writes (tmp + rename) so a partial download can't corrupt the cache
By default, <SvgaPlayer> stops both its frame loop and any active audio whenever:
- The component unmounts.
- The native view leaves the visible window (e.g. you navigated away in a stack navigator on iOS / Android).
- The app moves to the background (
AppStatetransitions away from'active').
This is what you usually want for gift animations and one-shot effects: the user moves on, the audio stops with them. When the player comes back on screen the animation does not auto-resume; call play() on the ref or remount to restart.
Set playInBackground={true} if you want video-player-style behaviour, where audio (and the underlying playback state) survive backgrounding and navigation:
<SvgaPlayer source={url} playInBackground />| Symptom | Likely cause | Fix |
|---|---|---|
Cannot find module '@dasimems/react-native-svga' at runtime |
Native rebuild missed | Bare: re-run pod install and rebuild. Expo: re-run expo prebuild + expo run:* |
| Crashes/errors only in Expo Go | This package isn't supported by Expo Go | Switch to a development build (see Installation) |
| Animation never appears, no error | source URL unreachable / 404 |
Check the URL in a browser; listen on onError |
| Built-in audio not playing | muteBuiltInAudio is true, or sounds is non-empty |
Pass muteBuiltInAudio={false} |
| Disk cache filling up | setLimit not configured |
Call SvgaCache.setLimit(maxBytes) at startup |
- Image caching for non-bundle PNG sources (Lottie-style remote image overrides)
- Configurable network adapter (drop in OkHttp /
fetchproxy) - Optional pre-decoded frame buffers for ultra-low-latency replays
npm install
npm run nitrogen
npm run typecheck
npm run lint
npm testRun the example app from example/:
cd example
npm install
npm run android # or: npm run iosMIT © dasimems
Built with Nitro Modules and create-react-native-library.