Skip to content

Open and control native webviews (WKWebView for macOS / WebView2 for Windows) from node/bun

License

Notifications You must be signed in to change notification settings

fcannizzaro/native-window

Repository files navigation

native-window

native-window

CI npm npm npm npm

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.

Features

  • 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

Packages

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

Quick Start

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);
});

Typed IPC

Use native-window-ipc for compile-time checked messaging between Bun and the webview. Schemas provide both types and runtime validation.

Host side (Bun/Node)

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>`);

Webview side (inline 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>

Webview side (bundled app)

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;
});

Runtime Detection

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.

API Reference

native-window

init()

Initialize the native window system. Must be called once before creating any windows.

pumpEvents()

Process pending native UI events. Call periodically (~16ms via setInterval) to keep windows responsive.

run(intervalMs?: number): () => void

Convenience: calls init() then starts a pumpEvents() interval. Returns a cleanup function.

checkRuntime(): RuntimeInfo

Check if the native webview runtime is available. Returns { available: boolean, version?: string, platform: "macos" | "windows" | "linux" | "unsupported" }.

ensureRuntime(): RuntimeInfo

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.

new NativeWindow(options?)

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

native-window-ipc

createChannel<S>(win, options): NativeWindowChannel<InferSchemaMap<S>>

Wrap an existing NativeWindow with a typed message channel. Schemas are required. Auto-injects the webview client script (disable with { injectClient: false }).

createWindow<S>(windowOptions, channelOptions): NativeWindowChannel<InferSchemaMap<S>>

Convenience: creates a NativeWindow and wraps it with createChannel.

getClientScript(): string

Returns the webview-side client as a self-contained JS string for manual injection.

createChannelClient<S>(options): TypedChannel<InferSchemaMap<S>> (from native-window-ipc/client)

Create a typed channel client inside the webview. Schemas are required. For use in bundled webview apps.

TypedChannel<T>

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;
}

Security

All security hardening is compiled in by default on all supported platforms — no build-time feature flags required.

  • URL scheme blockingjavascript:, file:, data:, and blob: navigations are blocked at the native layer
  • Content Security Policy — inject a CSP via the csp option in WindowOptions
  • 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 hardeningwindow.ipc and window.__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.

Building

Prerequisites

  • Bun (v1.3+)
  • Rust (stable)
  • macOS, Windows, or Linux (for native compilation)
  • On Linux: WebKitGTK development headers (e.g. libwebkit2gtk-4.1-dev on Ubuntu/Debian)

Install dependencies

bun install

Build the native addon

cd packages/native-window
bun run build          # release build
bun run build:debug    # debug build

The build targets the current platform. Cross-compilation targets are configured in packages/native-window/package.json under napi.triples.

Samples

# Raw IPC example
bun samples/basic.ts

# Typed IPC example
bun samples/typed-ipc.ts

Testing

# Run the IPC channel tests
cd packages/native-window-ipc
bun test

Known Limitations

  • ~16ms event latency from the pumpEvents() polling interval
  • HTML null origin — content loaded via loadHtml() has a null CORS origin; use a custom protocol or loadUrl() 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-dev on Ubuntu/Debian)
  • No return values from unsafe.evaluateJs() — use postMessage/onMessage to send results back
  • 2 MB HTML limit on Windows when using loadHtml()
  • Use bun --watch instead of bun --hot for development (native addon reloading requires a process restart)

License

MIT

About

Open and control native webviews (WKWebView for macOS / WebView2 for Windows) from node/bun

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published