Interactive Spline 3D scenes in React Native — fully native, no WebView.
Powered by the official Spline runtimes for iOS and Android, wrapped as an Expo native module with a zero-config plugin.
<SplineView url="https://build.spline.design/.../scene.splineswift" style={{ flex: 1 }} />That's it. A real, GPU-rendered, interactive 3D scene in your app.
| 🚀 Truly native | Renders through Spline's official Swift and Android runtimes — not a WebView. Full 60fps, native gestures, native memory. |
| 🔌 Zero-config setup | One config plugin wires up the SplineRuntime Swift Package, Gradle dependencies, permissions, and deployment targets. expo prebuild and go. |
| 🎮 Full scene control | Emit events, play/pause, zoom, rotate objects, recolor the background — all from JavaScript via useSpline(). |
| 🔄 Reactive variables | useVariable() is useState for your Spline scene — read and write scene variables with React ergonomics. |
| 📱 Gyroscope look-at | On iOS, objects with a cursor "Look At" interaction automatically follow device motion. Your scene literally looks back at the user. |
| 🧩 TypeScript first | Every event, object, and prop is fully typed. |
- A recent Expo SDK — developed against SDK 55 (or bare React Native with Expo Modules installed)
- iOS 16.0+ (required by Spline's iOS runtime)
- A hosted
.splineswiftscene URL — export via Spline → Export → Code → Swift (docs)
1. Install the package
npx expo install react-native-spline
# or
bun add react-native-spline2. Add the config plugin to app.json / app.config.js:
{
"expo": {
"plugins": ["react-native-spline"]
}
}3. Prebuild and run
npx expo prebuild
npx expo run:ios # or run:androidWhat does the config plugin do?
iOS
- sets the deployment target to iOS 16.0 (Podfile properties + Xcode project)
- adds the
SplineRuntimeSwift Package to your Xcode project - patches Podfile framework search paths so the module can
import SplineRuntime
Android
- adds
design.spline:spline-runtime:0.2.3 - adds
androidx.lifecycle:lifecycle-common-java8:2.6.2andandroidx.lifecycle:lifecycle-runtime-ktx:2.6.2 - ensures
INTERNETandACCESS_NETWORK_STATEpermissions are declared
Everything is idempotent — re-running prebuild never duplicates entries.
Using bare React Native (no prebuild)?
First install Expo Modules, then apply the native setup by hand:
iOS — add the SplineRuntime Swift Package (https://github.com/splinetool/spline-ios) in Xcode, set your deployment target to 16.0+, then:
npx pod-installAndroid — add to your app module's build.gradle:
dependencies {
implementation("design.spline:spline-runtime:0.2.3")
implementation("androidx.lifecycle:lifecycle-common-java8:2.6.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
}and declare in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />import { useEffect, useState } from 'react';
import { SafeAreaView, Text } from 'react-native';
import {
SplineView,
useSpline,
type SplineEventPayload,
} from 'react-native-spline';
export default function App() {
const { addEventListener, emitEvent } = useSpline();
const [latestEvent, setLatestEvent] = useState<SplineEventPayload | null>(null);
useEffect(() => {
const subscription = addEventListener('mouseUp', setLatestEvent);
return () => subscription.remove();
}, [addEventListener]);
return (
<SafeAreaView style={{ flex: 1 }}>
<SplineView
url="https://build.spline.design/MEB5dcTLGkXFu2uqul4b/scene.splineswift"
onLoad={() => emitEvent('start', 'Cube')}
style={{ flex: 1 }}
/>
<Text style={{ padding: 16 }}>
{latestEvent
? `${latestEvent.event} on ${latestEvent.objectName ?? latestEvent.objectId ?? 'unknown'}`
: 'Interact with the scene'}
</Text>
</SafeAreaView>
);
}The component that renders your scene.
| Prop | Type | Description |
|---|---|---|
url |
string |
Required. Hosted .splineswift scene URL. |
onLoad |
(event) => void |
Required. Fires when the scene finishes loading. event.nativeEvent.url echoes the scene URL. |
onSplineEvent |
(event) => void |
Fires for every Spline interaction event (event.nativeEvent is a SplineEventPayload). |
useDeviceGyroscopeForLookAt |
boolean |
iOS only, default true. Drives "Look At" objects with device motion. |
gyroscopeLookAtObjectIds |
string[] |
Explicit object UUIDs to drive with the gyroscope (skips scene auto-detection). |
style |
StyleProp<ViewStyle> |
Standard view styling. |
A hook returning stable, memoized controls for the active scene — mirrors Spline's Code API.
| Method | Signature | Description |
|---|---|---|
emitEvent |
(event, nameOrUUID) => void |
Trigger an interaction event on an object. |
emitEventReverse |
(event, nameOrUUID) => void |
Trigger the event's reverse animation. |
findObjectById |
(uuid) => Promise<SplineObject | null> |
Look up an object by UUID. |
findObjectByName |
(name) => Promise<SplineObject | null> |
Look up an object by name. |
setObjectRotation |
(nameOrUUID, { x, y, z }) => void |
Set an object's rotation (radians). |
setZoom |
(value) => void |
Set the camera zoom level. |
setBackgroundColor |
({ r, g, b, a }) => void |
Set the scene background (RGBA, 0–255). |
play / stop |
() => void |
Resume / pause scene playback. |
addEventListener |
(event, callback) => Subscription |
Subscribe to a scene event. Call .remove() on cleanup. |
const { setZoom, play, findObjectByName } = useSpline();
async function focusScene() {
setZoom(1.2);
play();
const cube = await findObjectByName('Cube');
console.log(cube?.rotation);
}Tip
Scene methods act on the currently loaded view. Call them after onLoad, or from event handlers and effects that run once the scene is mounted.
useState, but wired to a Spline scene variable. Reads the native value on mount, then keeps React state and the scene in sync on every set.
const [opacity, setOpacity] = useVariable<number>('opacity', 1);
const [visible, setVisible] = useVariable<boolean>('visible', true);
const [label, setLabel] = useVariable<string>('label', '');SplineEventPayload — { event, objectName?, objectId? }
Supported events: mouseUp · mouseDown · mousePress · mouseHover · keyUp · keyDown · keyPress · start · lookAt · follow
If your scene has objects with a cursor Look At interaction, SplineView decodes the .splineswift file, finds them automatically, and rotates them with device motion — so a character that follows the cursor on the web follows the phone's tilt in your app. Disable with useDeviceGyroscopeForLookAt={false}, or pin specific objects with gyroscopeLookAtObjectIds.
A full Expo example lives in example/:
bun install
bun run start:ios # or start:androidimport SplineRuntime fails on iOS
Re-run npx expo prebuild --clean so the config plugin can patch the freshly generated project, then reopen the workspace and build once from Xcode to let SPM resolve packages.
Scene doesn't load / blank view
Make sure the URL is a .splineswift export (not .splinecode, which is the web export) and that the device has network access. The scene must be hosted (Spline's "Export → Code → Swift" gives you the URL).
Android build errors about lifecycle classes
Verify the config plugin ran (check android/app/build.gradle for the design.spline:spline-runtime dependency). If you manage native projects manually, add the three Gradle dependencies listed in the bare setup section.
PRs welcome! This repo uses bun, Biome, and Conventional Commits (enforced by commitlint).
bun install # install deps
bun run check # lint + typecheck
bun run test # jest
bun run build # compile to build/Repository layout:
src/ TypeScript public API (SplineView, useSpline, useVariable)
ios/ Swift Expo module bridging SplineRuntime
android/ Kotlin Expo module bridging the Android Spline runtime
plugin/ Expo config plugin (SPM + Gradle + permissions injection)
example/ Expo example app
Releases are automated with semantic-release — merge to main with a conventional commit and CI handles versioning, changelog, npm publish, and the GitHub release.