Note
This project is in beta. APIs may change without notice and some features may be incomplete or unstable.
Native OS webviews for Bun, Deno & Node.js. Create real desktop windows with embedded web content using wry + tao — providing WebKit on macOS and Linux, and WebView2 on Windows.
- Native webviews — powered by wry + tao (WebKit on macOS/Linux, WebView2 on Windows), no Electron or Chromium bundled
- Multi-window — create and manage multiple independent windows
- HTML & URL loading — load inline HTML strings or navigate to URLs
- Bidirectional IPC — send messages between Bun/Deno/Node and the webview
- Typesafe IPC channels — typed message layer with schema-based validation and compile-time checked event maps
- Full window control — title, size, position, min/max size, decorations, transparency, always-on-top
- Window events — close, resize, move, focus, blur, page load, title change
- Rust + napi-rs + wry + tao — high-performance native addon, no runtime overhead
- Runtime detection — check for WebView2 availability and auto-install on Windows
| Package | Description |
|---|---|
@nativewindow/webview | Rust napi-rs addon providing native window + webview APIs |
@nativewindow/ipc | Pure TypeScript typesafe IPC channel layer |
@nativewindow/react | React bindings for the typed IPC layer |
@nativewindow/tsdb | TanStack DB collection adapter for native-window IPC |
import{NativeWindow}from"native-window";constwin=newNativeWindow({title: "My App",width: 800,height: 600,});win.loadHtml(` <h1>Hello from native webview!</h1> <button onclick="window.ipc.postMessage('clicked')">Click me</button>`);win.onMessage((msg)=>{console.log("From webview:",msg);win.postMessage(`Echo: ${msg}`);});win.onClose(()=>{process.exit(0);});Use native-window-ipc for compile-time checked messaging between Bun/Deno/Node and the webview. Schemas provide both types and runtime validation.
import{z}from"zod";import{createWindow}from"@nativewindow/ipc";constch=createWindow({title: "Typed IPC"},{schemas: {// host -> webviewhost: {"update-title": z.string(),},// webview -> hostclient: {"user-click": z.object({x: z.number(),y: z.number()}),counter: z.number(),},},},);// Receive typed messages from the webview (client events)ch.on("user-click",(pos)=>{// pos: { x: number; y: number }console.log(`Click at ${pos.x}, ${pos.y}`);});ch.on("counter",(n)=>{// n: numberch.send("update-title",`Count: ${n}`);});// ch.send("counter", "wrong"); // Type error: "counter" is a client event// ch.send("typo", 123); // Type error: "typo" does not existch.window.loadHtml(`<html>...</html>`);The __channel__ object is auto-injected by createWindow / createChannel:
<script>__channel__.send("user-click",{x: 10,y: 20});__channel__.on("update-title",(title)=>{document.title=title;});</script>For webview apps bundled with their own build step, import the client directly:
import{z}from"zod";import{createChannelClient}from"@nativewindow/ipc/client";constch=createChannelClient({schemas: {host: {"update-title": z.string(),},client: {counter: z.number(),},},});// Send client events to the hostch.send("counter",42);// Typed!// Receive host events from the hostch.on("update-title",(t)=>{// t: stringdocument.title=t;});On Windows 10, the WebView2 runtime may not be installed. Use checkRuntime() to detect it and ensureRuntime() to auto-install if missing.
import{checkRuntime,ensureRuntime}from"native-window";constinfo=checkRuntime();console.log(info);// { available: true, version: "128.0.2739.42", platform: "windows" }// { available: false, version: undefined, platform: "windows" }// { available: true, version: undefined, platform: "macos" }// { available: true, version: undefined, platform: "linux" }if(!info.available){console.log("WebView2 not found, installing...");constresult=ensureRuntime();// downloads ~2MB bootstrapper, runs silentlyconsole.log("Installed:",result.version);}On macOS, both functions return { available: true } immediately — WKWebView is a system framework. On Linux, both functions also return { available: true } — WebKitGTK is assumed to be installed. On Windows 11, WebView2 is pre-installed.
Check if the native webview runtime is available. Returns { available: boolean, version?: string, platform: "macos" | "windows" | "linux" | "unsupported" }.
Check for the runtime and install it if missing (Windows only). Downloads the WebView2 Evergreen Bootstrapper (~2MB) from Microsoft and runs it silently. Throws on failure.
Create a native window with an embedded webview.
WindowOptions:
| Option | Type | Default | Description |
|---|---|---|---|
title | string | "" | Window title |
width | number | 800 | Inner width (logical pixels) |
height | number | 600 | Inner height (logical pixels) |
x | number | — | X position |
y | number | — | Y position |
minWidth / minHeight | number | — | Minimum size |
maxWidth / maxHeight | number | — | Maximum size |
resizable | boolean | true | Allow resizing |
decorations | boolean | true | Show title bar and borders |
transparent | boolean | false | Transparent background |
alwaysOnTop | boolean | false | Float above other windows |
visible | boolean | true | Initially visible |
devtools | boolean | false | Enable devtools |
Content methods:
| Method | Description |
|---|---|
loadUrl(url) | Navigate to a URL |
loadHtml(html) | Load an HTML string |
unsafe.evaluateJs(script) | Execute JS in the webview (fire-and-forget) |
postMessage(msg) | Send a string to the webview via window.__native_message__ |
Window control:
| Method | Description |
|---|---|
setTitle(title) | Set the window title |
setSize(w, h) | Set the window size |
setMinSize(w, h) / setMaxSize(w, h) | Set size constraints |
setPosition(x, y) | Set window position |
setResizable(bool) | Toggle resizability |
setDecorations(bool) | Toggle decorations |
setAlwaysOnTop(bool) | Toggle always-on-top |
show() / hide() | Show or hide the window |
close() | Close and destroy the window |
focus() | Bring the window to focus |
maximize() / minimize() / unmaximize() | Window state |
Events:
| Method | Callback signature |
|---|---|
onMessage(cb) | (message: string) => void |
onClose(cb) | () => void |
onResize(cb) | (width: number, height: number) => void |
onMove(cb) | (x: number, y: number) => void |
onFocus(cb) / onBlur(cb) | () => void |
onPageLoad(cb) | (event: "started" | "finished", url: string) => void |
onTitleChanged(cb) | (title: string) => void |
Wrap an existing NativeWindow with a typed message channel. Schemas are required. Auto-injects the webview client script (disable with { injectClient: false }).
Convenience: creates a NativeWindow and wraps it with createChannel.
Returns the webview-side client as a self-contained JS string for manual injection.
Create a typed channel client inside the webview. Schemas are required. For use in bundled webview apps.
interfaceTypedChannel<TextendsEventMap>{send<KextendskeyofT&string>(type: K,payload: T[K]): void;on<KextendskeyofT&string>(type: K,handler: (payload: T[K])=>void): void;off<KextendskeyofT&string>(type: K,handler: (payload: T[K])=>void): void;}All security hardening is compiled in by default on all supported platforms — no build-time feature flags required.
- URL scheme blocking —
javascript:,file:,data:, andblob:navigations are blocked at the native layer - Content Security Policy — inject a CSP via the
cspoption inWindowOptions - Trusted origin filtering — restrict IPC messages and client injection to specific origins at the native and IPC layers
- Webview surface hardening — context menus, status bar, and built-in error page are disabled on Windows
- IPC bridge hardening —
window.ipcandwindow.__channel__are frozen, non-writable objects - Message size limits — 10 MB hard limit at the native layer, configurable 1 MB default at the IPC layer
- Schema-based validation — all incoming IPC payloads are validated at runtime against user-defined schemas
See the Security documentation for the full threat model and best practices.
- Bun (v1.3+), Deno (v2+), or Node.js (v18+)
- Rust (stable)
- macOS, Windows, or Linux (for native compilation)
- On Linux: WebKitGTK development headers (e.g.
libwebkit2gtk-4.1-devon Ubuntu/Debian)
bun install
# or
deno installcd packages/webview
bun run build # release build
bun run build:debug # debug buildThe build targets the current platform. Cross-compilation targets are configured in packages/webview/package.json under napi.triples.
# Raw IPC example
bun samples/basic.ts
# Typed IPC example
bun samples/typed-ipc.ts# Run the IPC channel testscd packages/ipc
bun test- ~16ms event latency from the automatic native event pump interval
- HTML null origin — content loaded via
loadHtml()has a null CORS origin; use a custom protocol orloadUrl()for fetch/XHR - Windows 10 may require the WebView2 Runtime — use
ensureRuntime()to auto-install (included by default on Windows 11) - Linux requires WebKitGTK to be installed (e.g.
libwebkit2gtk-4.1-devon Ubuntu/Debian) - No return values from
unsafe.evaluateJs()— usepostMessage/onMessageto send results back - 2 MB HTML limit on Windows when using
loadHtml() - Use
bun --watchinstead ofbun --hotfor development (native addon reloading requires a process restart)
MIT
