React Native for Linux desktop. Renders to GTK4, runs JS on Hermes, uses the new architecture (Fabric + TurboModules) from day one. Inspired by and structurally modeled after microsoft/react-native-windows.
Status: alpha. Real third-party RN libraries are starting to render —
react-native-papermounts and the floating-label animation in<TextInput mode="outlined">works end-to-end. The runtime is not yet production-ready. See the what works / what doesn't tables below and TODO.md for the live roadmap.
| Surface | Status |
|---|---|
<View> (GtkFixed) |
✅ flex layout, padding, margin, backgroundColor, borderRadius, opacity |
<Text> / <Paragraph> (GtkLabel) |
✅ font / color / alignment / numberOfLines; Yoga padding pushed in as CSS |
<Image> (GtkPicture) |
✅ http(s) / file:// / data: sources via libsoup3, resizeMode, tintColor via custom GdkPaintable + color matrix |
<ScrollView> (GtkScrolledWindow) |
✅ vertical + horizontal scroll, onScroll |
<TextInput> (GtkText) |
✅ value, placeholder, onChangeText, onSubmitEditing, onKeyPress, onFocus/Blur |
<Switch> (GtkSwitch) |
✅ value, onValueChange, disabled, tint colors |
<ActivityIndicator> (GtkSpinner) |
✅ animating, size |
<FlatList> / <SectionList> |
✅ via the upstream JS impl on top of ScrollView |
<Pressable> / <Button> |
✅ onPress, onLongPress, onHoverIn / onHoverOut (also onMouseEnter / onMouseLeave aliases), function-children render prop |
<Modal> |
✅ overlay + backdrop |
transform |
✅ translate / scale / rotate / matrix / origin (CSS transforms on GtkFixed children) |
onLayout |
✅ dispatched from LinuxComponentView::updateLayoutMetrics |
Animated (JS driver) |
✅ timing / spring (damped harmonic) / decay (closed-form) / sequence / parallel / loop / stagger / delay / event (argMapping → AnimatedValue) / ValueXY with extract/flatten offset semantics; interpolate does real RGB lerp for colour outputRange |
Animated (native driver) |
✅ translateX/Y/scale/scaleX/Y/opacity drive gtk_fixed_set_child_transform per frame, microtask-coalesced into one setNativeProps per host per frame so multi-prop transforms rebuild the GskTransform once |
RefreshControl |
✅ GtkScrolledWindow::edge-overshot (top) → onRefresh; refreshing prop mirrors into the C++ view so one gesture = one onRefresh |
| Fast Refresh | ✅ HMR socket auto-reloads the app bundle; React state preserved across refreshes |
async () => … in user code |
✅ esbuild lowers async / async-generator to generators so Hermes 0.12 accepts the bundle |
Linking.openURL / canOpenURL |
✅ g_app_info_launch_default_for_uri + g_app_info_get_default_for_uri_scheme |
Alert.alert / Alert.prompt |
✅ Alert.alert → GtkAlertDialog; Alert.prompt → hand-rolled modal GtkWindow + GtkEntry (Enter activates default button; secureEntry/'login-password' → password input purpose) |
Appearance / useColorScheme |
✅ reads RN_LINUX_COLOR_SCHEME env first, then GtkSettings::gtk-application-prefer-dark-theme |
Dimensions.get('window' | 'screen') |
✅ window via gdk_surface_get_*; screen via gdk_monitor_get_geometry for the monitor the window is on; resize listener updates both |
Cmd / Ctrl + R reload |
✅ GtkShortcutController triggers host->reload() — Mac VNC and Linux both work |
AsyncStorage |
✅ JSON file at $XDG_CONFIG_HOME/<applicationId>/async-storage.json — per-app sandbox |
| Per-app identity | ✅ applicationId baked at build time from consumer's package.json; AsyncStorage / SecureStore / KeepAwake / Notifications / FileSystem / DeviceInfo / Camera / Location / Image-cache all derive from it |
expo-modules-core shim |
✅ 26 in-tree shims register through globalThis.expo.modules; requireNativeModule(name) resolves transparently for third-party Expo packages |
react-native-device-info |
✅ all Windows-supported methods return real values from /sys, /proc, /etc |
react-native-paper (V3) |
✅ Card, TextInput.Outlined, Switch, Snackbar mount and interact |
| Surface | Status |
|---|---|
react-native-safe-area-context |
⚠ shim returns zero insets; SafeAreaProvider is a passthrough <View> |
| Third-party Expo packages from npm | ⚠ expo-modules-core shim resolves; modules must register through registerExpoModule(name, impl) to flow through it |
| Hermes 0.12 + native async / await | ⚠ Hermes rejects raw async / await syntax; the bundler lowers it to generators so user code can use it freely |
| Multi-window per running app | ❌ one GtkApplicationWindow per process today; multi-window via SurfaceHandler is design-doc'd in docs/design-multi-instance |
Fabric EventEmitter event dispatch |
⚠ events still route through per-tag JSI registries; real EventEmitter plumbing is design-doc'd in docs/design-fabric-event-emitter |
| TurboModule autolinking | ❌ @react-native/codegen pipeline not wired yet; design-doc'd in docs/design-turbomodule-manager. In-tree shims work through ad-hoc rnLinux.* bindings instead |
| vnext unit-test binary in CI | ❌ Hermes' bundled llvh gtest collides with upstream googletest; tests build locally via a Hermes-free configure |
- Make
npx @react-native-community/cli init MyAppwork withreact-native-linuxadded, thenpnpm react-native run-linuxopen a GTK4 window that renders<View><Text>Hello</Text></View>. - Keep the JS authoring experience identical to React Native on
iOS / Android / Windows.
Platform.OS === 'linux'; everything else is RN. - Stay Fabric-only — no legacy bridge, no legacy view managers.
react-native-linux/
├── packages/@lucid-softworks/
│ ├── react-native-linux/ # JS-side npm package (Platform shim, components)
│ ├── react-native-linux-expo/ # Stubs for the expo packages most RN apps drag in
│ └── cli/ # CLI plugin: run-linux, bundle-linux, init-linux
├── vnext/ # Native C++ runtime (CMake)
│ ├── include/react-native-linux/ # Public headers
│ ├── src/ # Implementation
│ │ ├── fabric/ # Fabric mounting + component view registry
│ │ ├── views/ # GTK widget bindings (View, Text, Image, …)
│ │ ├── jsi/ # Hermes runtime + JSI bindings + bundle loader
│ │ ├── storage/ # AsyncStorage backing (JSON file)
│ │ ├── deviceinfo/ # react-native-device-info native backing
│ │ ├── modules/ # PlatformConstants + other TurboModule stubs
│ │ └── devtools/ # MetroReloadClient (HMR socket)
│ └── cmake/ # Helper modules (FetchHermes, FindGTK4, …)
├── template/ # `react-native init` template
│ ├── App.tsx
│ └── linux/ # Linux project files copied into apps
├── apps/playground/ # First-party playground app + smoke tests
│ ├── App.tsx # Demo gallery (FlatList, Modal, Animated, …)
│ ├── paper-demo.tsx # Real-app harness: react-native-paper
│ ├── smoke-demo.tsx # Probe + live UI for 5 next-batch libraries
│ ├── runtime/ # React + reconciler + shims (vendor bundle)
│ └── linux/ # CMake app glue (main.cpp, autolinked.cmake)
├── docs/ # Architecture, getting-started, real-app gap lists
│ ├── architecture.md
│ ├── component-support.md
│ ├── dev-vm.md
│ ├── native-modules.md
│ ├── packaging.md
│ ├── realworld-paper.md # gaps surfaced by react-native-paper
│ ├── realworld-expo-location.md # GeoClue2-backed expo-location shim
│ ├── realworld-expo-camera.md # GStreamer-backed expo-camera shim
│ ├── realworld-expo-notifications.md # libnotify-backed expo-notifications shim
│ ├── realworld-expo-file-system.md # POSIX-backed expo-file-system shim
│ ├── realworld-expo-clipboard.md # GdkClipboard-backed expo-clipboard shim
│ ├── realworld-expo-secure-store.md # libsecret-backed expo-secure-store shim
│ ├── realworld-expo-localization.md # libc + sysfs expo-localization shim
│ ├── realworld-expo-haptics.md # gdk_display_beep-backed expo-haptics shim
│ ├── realworld-expo-keep-awake.md # logind-Inhibit-backed expo-keep-awake shim
│ ├── realworld-expo-network.md # GNetworkMonitor + sysfs expo-network shim
│ └── …
├── scripts/vm/ # Lima dev VM helpers (start.sh, sh.sh, …)
└── TODO.md # Live roadmap
| Concern | Choice |
|---|---|
| UI toolkit | GTK4 |
| JS engine | Hermes (built from source via CMake) |
| Architecture | Fabric + TurboModules only |
| Layout | Yoga (bundled with RN) |
| Package manager | pnpm 9 |
| Build system | CMake (Ninja) |
| Minimum RN | ^0.81 |
| Minimum distro | Ubuntu 24.04 LTS (CI gates on this) |
| License | MIT |
# In an existing RN app
pnpm add -D @lucid-softworks/react-native-linux @lucid-softworks/react-native-linux-cli
pnpm react-native init-linux
pnpm react-native run-linuxUntil that's reliable, run the in-tree playground (see Developing below).
sudo apt install \
build-essential cmake ninja-build pkg-config \
libgtk-4-dev libsoup-3.0-dev \
python3 python3-pip \
nodejs npm
corepack enable
corepack prepare pnpm@9.15.5 --activateHermes, Folly, Glog, fmt, double-conversion, and a Boost subset are fetched and built by CMake — no system packages needed for those.
A scripted Lima VM gives macOS contributors a CI-parity Linux environment with a visible Xfce desktop over VNC. The full flow:
# One-time
scripts/vm/start.sh # boot the rn-linux Lima VM
open vnc://127.0.0.1:5901 # password: rnlinux
# Every iteration
scripts/vm/sh.sh 'cmake --build apps/playground/linux/build'
node apps/playground/bundle.mjs # rebuild the JS bundle
scripts/vm/sh.sh 'bash scripts/vm/run-playground.sh'scripts/vm/sh.sh is a thin wrapper around limactl shell --workdir /workspaces/react-native-linux so paths line up between macOS host and Linux
guest — without --workdir, limactl inherits the host cwd and the guest
spams bash: cd: …: No such file or directory before every command.
Switching demos:
# The default demo (FlatList / Modal / Animated / TextInput / …)
node apps/playground/bundle.mjs
# react-native-paper drop-in
RN_ENTRY=paper-demo.tsx node apps/playground/bundle.mjs
# Smoke test for next-batch libraries (async-storage, device-info, …)
RN_ENTRY=smoke-demo.tsx node apps/playground/bundle.mjsSee docs/dev-vm.md for VM internals and docs/getting-started.md for native-build details.
Each library below has its own gap list — what currently works, what breaks, and what's been fixed in the process.
- docs/realworld-paper.md —
react-native-paper@^5 - docs/realworld-expo-location.md —
expo-locationvia GeoClue2 - docs/realworld-expo-camera.md —
expo-cameravia GStreamer - docs/realworld-expo-notifications.md —
expo-notificationsvia libnotify - docs/realworld-expo-file-system.md —
expo-file-systemvia POSIX + libsoup - docs/realworld-expo-clipboard.md —
expo-clipboardvia GdkClipboard - docs/realworld-expo-secure-store.md —
expo-secure-storevia libsecret - docs/realworld-expo-localization.md —
expo-localizationvia libc + sysfs - docs/realworld-expo-haptics.md —
expo-hapticsvia gdk_display_beep - docs/realworld-expo-keep-awake.md —
expo-keep-awakevia systemd-logind Inhibit - docs/realworld-expo-network.md —
expo-networkvia GNetworkMonitor + sysfs - docs/realworld-expo-battery-sharing.md —
expo-battery+expo-sharing(JS-only) - docs/realworld-expo-pickers.md —
expo-document-picker+expo-image-pickervia GtkFileDialog - docs/realworld-expo-print.md —
expo-printvia GtkPrintOperation + Pango - docs/realworld-expo-screen-capture.md —
expo-screen-capture(honest no-op on Linux) - docs/realworld-expo-image.md —
expo-imageover RN.Image
The smoke-demo.tsx harness in apps/playground/ covers the next batch:
@react-native-async-storage/async-storage, react-native-device-info,
react-native-safe-area-context, expo-camera, expo-location.
See TODO.md for the prioritized work list. Each phase calls out its acceptance criteria. Open issues for design questions before sending large PRs.
Conventional Commits are required — release-please watches main for
feat: / fix: and produces release PRs that publish to npm on merge.
MIT. See LICENSE.