Run real React Native component trees inside any React DOM app — rendered
onto a <canvas> through Yoga (WASM) layout and Skia (CanvasKit) paint, with a
custom React reconciler. This is not react-native-web: there is no
translation to DOM components. Unmodified RN libraries run their actual native
code paths — real react-native-reanimated, real @gorhom/bottom-sheet, real
react-navigation (nested stack in bottom tabs, native-style headers and
transitions), real react-native-drawer-layout — and whole apps have run
byte-untouched (the engine's acceptance run was a full generated app from a
popular community template, zero source deviations).
Warning
EXPERIMENTAL — AND HEAVILY AI-DRIVEN
This codebase was built largely by AI.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nativeSurface } from 'native-surface/vite';
export default defineConfig({
plugins: [
...nativeSurface({ platform: 'ios', reanimated: 'real' }),
// Metro applies reanimated's Babel plugin everywhere; app source needs it too:
react({ babel: { plugins: ['react-native-reanimated/plugin'] } }),
],
});// Any React DOM component:
import { NativeSurface } from 'native-surface';
import { MyRNScreen } from './rn/MyRNScreen'; // ordinary React Native source
<NativeSurface width={390} height={720} onReady={() => console.log('painted')}>
<MyRNScreen />
</NativeSurface>The preset aliases react-native (and friends) to the engine, applies Metro's
platform-extension and react-native exports-condition resolution, transforms
require() assets, and workletizes RN libraries in node_modules (Metro
parity — reanimated worklets in published libraries don't work without it).
App-specific config stays yours: RN libraries that ship ESM and import
react-native must be listed in optimizeDeps.exclude (see
examples/embed-demo/vite.config.ts for a worked example with
@gorhom/bottom-sheet and react-navigation).
Use the optional native-surface/automation entry to capture
screenshots, inspect clipped structural snapshots, inject coordinate input, and
wait for painted changes or visual stability. Obtain the root through
<NativeSurface onReady={root => ...}> or rootRef. No automation or debug
globals are enabled by default. Existing drivers can explicitly pass debug
to enable the legacy __nativeSurfaceRoots registry.
Connect the controller to your application's WebSocket or IPC transport to let another process drive it. The library opens no connections or servers. Focused inputs and portal content require an external display capture provider when capturing the complete screen; see the automation guide.
- Native modules don't exist here. Camera, maps, purchases, push — out of scope. Components touching them need mocks.
- Remote images obey browser CORS. Hosts without
Access-Control-Allow-Originwon't load (a real device has no such restriction). - The
react-native-mmkvshim stores tolocalStorage— plaintext, like MMKV's default, but in the browser's storage. Don't put secrets in it. __nativeSurfaceRootsis a same-origin debug/driver surface. Any script on the page can inspect and drive surfaces — same trust model as the DOM.- A pass here is not a pass on a device. High fidelity is the goal and the record so far is good, but the shipped runtime is Hermes/Fabric.
| Package | What |
|---|---|
native-surface |
The engine, <NativeSurface>, and the native-surface/vite preset |
@native-surface/compat |
Boundary-general shims for RN community packages that need engine wiring (installed automatically) |
MIT © Ryan Lanciaux. Bundled Inter font © The Inter Project Authors, under the
SIL Open Font License 1.1 (assets/fonts/LICENSE-Inter-OFL.txt).