Warning
This project is in alpha. APIs may change without notice and some features may be incomplete or unstable.
Native OS webviews for Bun & 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/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 |
|---|---|
@fcannizzaro/native-window |
Rust napi-rs addon providing native window + webview APIs |
@fcannizzaro/native-window-ipc |
Pure TypeScript typesafe IPC channel layer |
@fcannizzaro/native-window-ipc-react |
React bindings for the typed IPC layer |
@fcannizzaro/native-window-tsdb |
TanStack DB collection adapter for native-window IPC |
import { init, pumpEvents, NativeWindow } from "native-window";
init();
const pump = setInterval(() => pumpEvents(), 16);
const win = new NativeWindow({
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(() => {
clearInterval(pump);
process.exit(0);
});Use native-window-ipc for compile-time checked messaging between Bun and the webview. Schemas provide both types and runtime validation.
import { z } from "zod";
import { createWindow } from "native-window-ipc";
const ch = createWindow(
{ title: "Typed IPC" },
{
schemas: {
"user-click": z.object({ x: z.number(), y: z.number() }),
"update-title": z.string(),
counter: z.number(),
},
},
);
ch.on("user-click", (pos) => { // pos: { x: number; y: number }
console.log(`Click at ${pos.x}, ${pos.y}`);
});
ch.on("counter", (n) => { // n: number
ch.send("update-title", `Count: ${n}`);
});
// ch.send("counter", "wrong"); // Type error!
// ch.send("typo", 123); // Type error!
ch.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 "native-window-ipc/client";
const ch = createChannelClient({
schemas: {
counter: z.number(),
"update-title": z.string(),
},
});
ch.send("counter", 42); // Typed!
ch.on("update-title", (t) => { // t: string
document.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";
const info = 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...");
const result = ensureRuntime(); // downloads ~2MB bootstrapper, runs silently
console.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.
Initialize the native window system. Must be called once before creating any windows.
Process pending native UI events. Call periodically (~16ms via setInterval) to keep windows responsive.
Convenience: calls init() then starts a pumpEvents() interval. Returns a cleanup function.
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.
interface TypedChannel<T extends EventMap> {
send<K extends keyof T & string>(type: K, payload: T[K]): void;
on<K extends keyof T & string>(type: K, handler: (payload: T[K]) => void): void;
off<K extends keyof T & 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+)
- Rust (stable)
- macOS, Windows, or Linux (for native compilation)
- On Linux: WebKitGTK development headers (e.g.
libwebkit2gtk-4.1-devon Ubuntu/Debian)
bun installcd packages/native-window
bun run build # release build
bun run build:debug # debug buildThe build targets the current platform. Cross-compilation targets are configured in packages/native-window/package.json under napi.triples.
# Raw IPC example
bun samples/basic.ts
# Typed IPC example
bun samples/typed-ipc.ts# Run the IPC channel tests
cd packages/native-window-ipc
bun test- ~16ms event latency from the
pumpEvents()polling 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
