Tauri 2 for a lightweight desktop app (OS native webview, tiny builds). The pick changes on need: deep Node/native integration, the biggest plugin ecosystem, or an existing Electron app goes to Electron; an existing web URL to wrap goes to Pake; no real OS integration needed goes to a PWA with no shell; React Native code rather than a web app goes to react-native-windows / react-native-macos.
wrap an existing web URL fast → Pake (Tauri under the hood)
new lightweight desktop app, fine writing some Rust → Tauri 2
deep Node/native integration, biggest plugin ecosystem, or existing Electron app → Electron
P2P / serverless / local-first desktop → Pear (Holepunch; the backend architecture is RB-E-P2P, depth in holepunch-p2p-systems)
web-first team also shipping mobile → Capacitor
no real OS integration needed → PWA (skip the shell)
existing REACT NATIVE codebase extending to desktop → react-native-windows / react-native-macos (native UI, shares RN code; check rn-macos version lag)
Linux-only app that should look like a Linux app → GTKX (React over GTK4, no webview; anything cross-platform goes back to the shells above)
Options & tradeoffs
the field considered — and why each one isn’t the default here
option
tradeoff
evidence
Electron
bundles its own Chromium + Node; largest ecosystem & most mature; deep Node/native access; heaviest install (~100–200MB)
9.6M/wk · ships in 2/20
Tauri (2.x)
Rust shell using the OS native webview (no bundled Chromium) → tiny builds (from ~600KB); desktop + mobile (iOS/Android); native logic in Rust. @tauri-apps/cli 2.11.5 (2026-09-20, verified vs npm)
Pake
one-command wrapper that turns a web URL into a desktop app on Tauri; <10MB (~20x smaller than Electron); fast path to ship an existing site, not a full app framework. pake-cli 3.17.3 (2026-10-05, verified vs npm + the V3.17.3 release): native capabilities are now scoped to the exact packaged origin — package the URL a site finally lands on
Pear (Holepunch)
P2P/serverless desktop+mobile runtime (Bare); no central server, content-addressed distribution; depth → holepunch-p2p-systems
42/wk
Capacitor (Ionic)
wraps a web app into iOS/Android (and desktop via Electron) native shells; web-first teams shipping to mobile
PWA (no shell)
installable from the browser; least native integration but zero packaging — try this before reaching for a shell
Native SDK (Vercel Labs) — EXPERIMENTAL
an OWNED engine drawing every pixel (neither bundled like Electron nor borrowed like Tauri): markup + TypeScript/Zig compiled to native at build time; TEA-style, NOT React. 3.6MB/~100ms calculator (vendor numbers); mobile experimental. Not picked while experimental; see note: NATIVE SDK
GTKX (1.x)
the LINUX-NATIVE React answer: a React 19 renderer over GTK4 (JSX creates GObject instances, no webview), run as an ordinary Node process; stock React, Linux ONLY by design. Needs Node ≥24 + GTK4/GLib dev packages. @gtkx/react 1.6.0 (2026-08-29, verified vs npm), moving fast — pin it; see note: GTKX
the RN-NATIVE desktop path — real native UI from RN code, no webview; for RN teams extending to desktop, not for packaging a web app. rn-windows 0.84.0 (2026-06-18, preview 0.85.0-preview.2) tracks core closely; rn-macos jumped 0.81→0.83.0 (2026-09-30, its first 0.83 release, syncing core RN 0.83.10) and lags core RN by one minor instead of three (verified vs npm + GitHub).
npm weekly downloads (from the corpus's last signals run): electron 9.6M · pear-runtime-react-native 42 · pear-runtime 2k · react-native-windows 120k · react-native-macos 114k
Verified notes
The durable selection axis is the renderer. Electron BUNDLES Chromium (large, uniform, maximum ecosystem); Tauri (2.x stable) BORROWS the OS native webview (tiny — from ~600KB — but rendering varies by platform webview) and adds iOS/Android; Pake and Pear also borrow. Three lanes skip the webview: react-native-windows/-macos render RN code natively, GTKX renders React to GTK4 on Linux, and Native SDK owns its renderer. The default is the lightest shell that grants the OS access you need. Tauri/Pake facts are verified vs tauri.app and the Pake repo; Electron/Capacitor/PWA rows are settled industry knowledge; the PWA-vs-Electron limits are sourced in the Zakharchenko reading (2026-10-08).
PAKE (2026-09-24, verified vs npm and the Pake README): a one-command web-URL→desktop wrapper built ON Tauri, under 10MB and ~20x smaller than an Electron equivalent; pake-cli 3.17.1 (2026-09-20). → Good for shipping an existing site, not for a from-scratch app. 3.17.3 (2026-10-05, verified vs the V3.17.3 release + npm) grants notifications, downloads, zoom, window controls and open-in-browser only to local pages and the exact origin of the packaged URL, so a page that navigates to another site loses them — package the final landing URL (the release's example: https://mail.google.com, not https://gmail.com).
RN DESKTOP (2026-07-10, verified vs npm + microsoft/react-native-windows releases; pins re-verified 2026-09-24): for teams whose CODE is React Native (not a web app), react-native-windows/-macos is the share-your-RN-code answer — a different axis than the bundled-vs-borrowed renderer. rn-windows is at 0.84.0, rn-macos at 0.81.9.
GTKX (2026-09-01, verified vs the 1.0, 1.1 and 1.5 posts and npm): the only Linux-NATIVE React row; every other row reaches Linux through a webview or not at all. JSX creates GObject instances; the app is an ordinary Node process (node:fs, fetch, npm all work; GTK and JS share one thread); hooks, context, Suspense, portals and the React Compiler behave as usual. Linux only, deliberately — that narrowness lets it expose the whole toolkit instead of a cross-platform subset. Needs Linux, Node ≥24 and the GTK4/GLib dev packages; `npm create gtkx`. 1.0 shipped 2026-08-04. 1.1 (2026-08-11) adds `gtkx deploy`, which generates the desktop entry + AppStream metainfo and builds .flatpak/.deb/.rpm/.AppImage from a config block instead of the eleven hand-written packaging files 1.0 required. 1.5 (blog 2026-08-27, no breaking changes) adds @gtkx/forms (React Hook Form adapter binding native Adwaita rows — Entry/PasswordEntry/Switch/Spin/ComboRow — with typed values and native invalid styling) and @gtkx/i18n (gettext POT/MO pipeline behind the real react-i18next API, with generated TS declarations enforcing message keys). @gtkx/react on npm is 1.6.0 (2026-08-29) while the blog is at 1.5 → pin it. Same shape as react-native-windows — share React knowledge, not a web app — with the platform scope inverted: one OS, whole toolkit.
NATIVE SDK (2026-07-17, verified vs native-sdk.dev/bridge + /platform-support; mobile status re-verified 2026-09-24): declarative markup + TypeScript/Zig compiled to native at BUILD time (no browser, no WebView, no JS engine in the binary); a TEA-style single update function, NOT React; 45+ built-in components and an embedded automation server for coding agents; desktop hosts run full apps, mobile is experimental. It ships a 16KiB-capped async-JSON WebView Bridge, and RN Rewind's critique stands quoted: async serialized JSON bridge + declarative views + native renderer ≈ "inventing React Native 0.59". → The owned-renderer lane is real but experimental; watch, don't bet.
EXPO-DESKTOP LEAD (2026-07-16, TWiR #284): shirakaba/expo-desktop targets Expo → macOS/Windows directly — early and unproven, but a third RN-desktop lane (vs Microsoft's forks) to watch.
CROSS-REFERENCES: RB-E-P2P owns the Pear backend architecture and the holepunch-p2p-systems skill owns its depth; RB-E-SECURITY owns shell security; RB-E-CROSSPLATFORM owns sharing code across web and native.
HISTORY:
2026-10-08 REOPENED: Pake 3.17.3 scopes native capabilities to the packaged origin; PWA-vs-Electron reading added (first source for the PWA row's limits).
2026-10-01 REOPENED: react-native-macos jumped 0.81.9 → 0.83.0 (first 0.83 release, syncs core RN 0.83.10), narrowing its lag behind react-native-windows (0.84.0) from three minors to one.
2026-09-24 CORRECTED: Pake pin V3.12.0 (2026-06-21) → pake-cli 3.17.1 (2026-09-20).
2026-08-18 GAP CLOSED: GTKX promoted skip→option when 1.0 shipped (the reopen signal wired at TWiR #291), filling the Linux-native React hole.
2026-07-17 GAP CLOSED: Native SDK promoted lead→option on a second independent signal in a week (RN Rewind #49), resolving twir-290's too-early reopen condition.
2026-07-10 GAP CLOSED: RN-desktop rows added (React Weekly); the entry had been web-shell-only.
Canonical reading
Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.
Six months moving an EPUB editor (Byte Binding) from Electron to a PWA and back. The PWA won on size, startup and updates, but lost on desktop basics: file associations and launch parameters blocked by open WICG bugs, APIs that differ or are missing in Safari, an installed PWA always opening in the browser it came from, no custom tray/dock menus or global hotkeys, Cmd+S/Cmd+Q reserved by the browser, no 'Open with' entry, and a hard-to-find install icon. His verdict: a PWA is a good addition to a website, not a desktop-app platform.
How Raycast ships one React + TypeScript codebase to both macOS and Windows via native shells (Swift/C#) plus WebKit — IPC design, memory breakdown, and concrete WebKit workarounds. A durable model for cross-platform desktop on React without Electron.
Depth (in-domain rules) is owned by the engineering-principles skill — this entry is selection breadth.
The full explanation
The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.
About desktop apps & web-to-native shells for React
Diataxis: Explanation. This page builds understanding of the desktop-shell
decision — the reasoning behind the pick. It is not a tutorial and not step-by-step
how-to: the candidate list and one-line tradeoffs live in the index entry RB-E-DESKTOP;
shell security depth lives in RB-E-SECURITY, and Pear (Holepunch) depth in the
holepunch-p2p-systems skill. Read this to understand why; read those to do.
React already runs on the desktop — the question is the shell
A "React desktop app" is almost never a special build of React. It is your ordinary
React-DOM app rendered inside a window that the operating system treats as a native
application — its own icon, dock/taskbar presence, menus, file associations, and OS APIs.
The React layer is unchanged. What you are actually choosing is the shell: the thin
native host that opens a window, points a web renderer at your UI, and brokers access to
the machine.
That reframing matters because it collapses most of the agonising. You are not picking a
UI framework here (RB-E-STYLING, RB-E-COMPONENT-LIBS already did that); you are picking
how much machinery to staple to a web app you mostly already have. The right default is
therefore the lightest shell that grants the OS access you actually need — and often
that is no shell at all (a PWA).
The one distinction that organises everything
Every desktop shell answers one question, and the answer drives size, consistency, and
risk: does it bring its own browser, or borrow the operating system's?
Bundled renderer (Electron). Ships its own copy of Chromium + Node.js inside
every app. Consequence: identical rendering on every OS and full Node/native power
— at the cost of a large install (often 100 MB+) and the memory of a whole browser per app.
Borrowed renderer (Tauri, Pake, Pear, Wails…). Uses the OS native webview already
on the machine — WebView2 (Chromium) on Windows, WKWebView (WebKit) on macOS, WebKitGTK
(WebKit) on Linux. Consequence: tiny installs (Tauri builds start around ~600 KB) — at
the cost that rendering now varies by platform webview, so the thing you must test is
exactly what Electron buys you for free.
A second, orthogonal axis is distribution: most shells assume a central server or store
to download from; Pear (Holepunch) instead distributes apps peer-to-peer and
content-addressed, with no central host. For the heartit ecosystem that axis, not size, is
the deciding one.
Hold those two axes — bundled vs borrowed renderer and centralised vs P2P distribution —
and the option list stops being a grab-bag.
The default, and why
Lightweight desktop → Tauri 2 (native webview, tiny builds). Just wrapping an
existing web URL → Pake.Need deep Node/native integration, the biggest plugin
ecosystem, or already on it → Electron.P2P / serverless / local-first → Pear.
And before any of these: if you don't need real OS integration, ship a PWA and skip the
shell entirely.
The ordering is deliberate. Start by asking whether you need a shell at all; then prefer the
borrowed-renderer path (small, modern) unless a concrete need — uniform rendering, a mature
plugin, or heavy Node use — pulls you to Electron. The verified facts behind the picks:
Tauri 2.x is stable, uses the OS webview, and spans desktop and mobile; Pake
is a one-command web-URL→desktop wrapper built on Tauri, landing
under 10 MB — roughly 20× smaller than the Electron equivalent (pake-cli 3.17.1,
2026-09-20). (Sources: tauri.app; the Pake repository; npm.)
The landscape, and when each one wins
Treat these as reasons to deviate from the default, not a menu to agonise over.
Electron — the incumbent. It wins on maturity and uniformity: one Chromium means
your app renders the same on Windows, macOS, and Linux, and the plugin/native-module
ecosystem is the largest by far. Choose it when you lean hard on Node in the main process,
need a specific mature integration, or already have an Electron app that works. The price is
size and per-app memory, and a security posture you must configure (below).
Tauri (2.x) — the modern lightweight default. Rust core + OS webview yields tiny,
fast-starting apps and a locked-down-by-default security model (explicit capabilities rather
than an open Node surface), plus a single codebase that also targets iOS/Android. The costs
are rendering variance across OS webviews (you must test each) and writing native logic
in Rust rather than Node. (Wails is the Go-based analog of the same borrowed-renderer
idea, if your native code is Go.)
Pake — not really a framework but a preset over Tauri: point it at a URL and it
emits a desktop app with an icon, dock presence, and shortcuts. It wins as the fastest path
to ship an existing website as a desktop client, and as proof of how small the
borrowed-renderer approach gets. It is the wrong tool the moment you need real app logic,
deep OS integration, or offline behaviour beyond what the site already does — at that point
you've outgrown the wrapper and want Tauri (or Electron) proper.
Pear (Holepunch) — a P2P application runtime (on the Bare JS runtime) that runs your
JS/React-DOM app with no central server: apps are distributed peer-to-peer and
content-addressed. It wins when serverless, local-first, or censorship-resistant
distribution is the point — the heartit desktop apps live here. You're trading the
familiar store/server deployment model for the P2P one; the depth of that model (identity,
discovery, replication, update channels) is owned by the holepunch-p2p-systems skill.
Capacitor (Ionic) — the same borrowed-renderer idea aimed primarily at mobile
(iOS/Android), with desktop reached via an Electron target. It wins for a web-first team
that wants one wrapped web app across mobile and desktop and is comfortable in the Ionic
toolchain. For desktop specifically, it's an indirection over Electron rather than a
lighter option.
PWA (no shell) — the browser is the shell. Installable from the address bar, auto-
updating, zero packaging or signing. It wins whenever you don't truly need deep OS access:
it is the lightest possible "desktop app." Its ceiling is the browser sandbox — limited
filesystem, no arbitrary native APIs, weaker offline/background story than a real shell.
Try this first; reach for a shell only when you hit that ceiling.
Three lanes with no webview at all. If your code is React Native rather than a web
app, the entry points to react-native-windows / react-native-macos — real native UI from
RN code (rn-windows 0.84.0 tracks core closely; rn-macos 0.81.9 lags). For a Linux-only
app that should look like a Linux app, GTKX renders stock React to GTK4 as an ordinary
Node process — no webview, Linux only by design, moving fast enough to pin. Native SDK
(Vercel Labs) owns its renderer and compiles markup + TypeScript/Zig to native, but it is not
React and its mobile support is experimental, so the entry lists it without picking it.
Tradeoffs and failure modes to name out loud
Reaching for Electron by reflex. Shipping 150 MB and a full Chromium to host what is
effectively a bookmark is the classic over-build. If you don't use Node or deep OS APIs, a
PWA or Pake does the job at a fraction of the weight.
Assuming a borrowed renderer renders identically everywhere. Tauri/Pake/Pear use the
OS webview, so a CSS/JS quirk can appear on Linux's WebKitGTK but not on Windows'
Chromium-based WebView2. Uniform rendering is precisely the thing you gave up for the small
size — budget cross-OS testing. (This cuts the other way too: it's the single best reason
to stay on Electron.)
Forking your UI in the shell. The whole value is that desktop is just another target
of the same React-DOM app (RB-E-CROSSPLATFORM). Keep the shell a thin host; if real
product logic migrates into Electron's main process or Tauri's Rust side, you've grown a
second app to maintain.
Trusting web content with native power. A desktop shell deliberately punches a hole
from web content to the machine. Electron's historic footgun is leaving Node reachable
from the renderer (mitigate with context isolation, sandboxing, and an explicit IPC
surface); Tauri inverts the default with an opt-in capability allowlist. Either way the
bridge is the attack surface — depth on this is owned by RB-E-SECURITY.
How it interacts with the rest of the stack
Cross-platform (RB-E-CROSSPLATFORM). Desktop is the web target wearing a native
coat: the same shared logic/state/data package and the same React-DOM UI run inside the
shell. This entry is "how do I package the web build as an app"; that entry is "how do I
share code across web and native."
Meta-frameworks & build (RB-E-META-FRAMEWORKS, RB-E-BUILD). Whatever produces your
web bundle feeds the shell. Note that RSC/SSR meta-frameworks assume a server, which a
shipped desktop client may not have — a desktop shell usually wants a client-rendered
build (SPA/static) it can load locally, which is also why Pake (pure URL wrap) is so cheap.
Security (RB-E-SECURITY). The renderer↔native bridge, IPC allowlists, and
content-security posture of the shell are security concerns, not packaging trivia.
P2P runtime depth. Pear's identity, discovery, replication, and update model are owned
by the holepunch-p2p-systems skill — this page only places Pear on the map.
In one paragraph
A React desktop app is your web app in a native window; the real choice is the shell, and
it turns on one question — bundle a browser or borrow the OS webview. Electron bundles
Chromium for uniform rendering and full Node power at ~100 MB+; Tauri 2 borrows the OS webview
for tiny, modern, more-locked-down apps (test per-OS, write Rust for native bits); Pake is the
one-command way to wrap an existing URL on Tauri; Pear swaps centralised distribution for P2P
and is heartit's path; Capacitor suits web-first teams already shipping mobile; and a PWA
is the lightest "desktop app" of all — start there and only add a shell when you hit the
sandbox's ceiling. Keep the shell thin, test the borrowed renderer across platforms, and treat
the web↔native bridge as an attack surface.