Skip to content

Repository files navigation

native-surface

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.

Quickstart (Vite)

// 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).

Remote control

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.

Limits

  • 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-Origin won't load (a real device has no such restriction).
  • The react-native-mmkv shim stores to localStorage — plaintext, like MMKV's default, but in the browser's storage. Don't put secrets in it.
  • __nativeSurfaceRoots is 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.

Packages

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)

License

MIT © Ryan Lanciaux. Bundled Inter font © The Inter Project Authors, under the SIL Open Font License 1.1 (assets/fonts/LICENSE-Inter-OFL.txt).

About

Embed real React Native (not RN web) in any React web app

Topics

Resources

Stars

12 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages