Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/webviewjs/webview/llms.txt

Use this file to discover all available pages before exploring further.

Inter-process communication in WebviewJS is built on the window.ipc global that wry automatically injects into every page. Messages travel in both directions: the page posts raw bytes to Node with window.ipc.postMessage(), and Node pushes JavaScript expressions back into the page with evaluateScript() or reads return values with evaluateScriptWithCallback(). For structured, Promise-based two-way calls the higher-level webview.expose() API wraps all of that boilerplate behind typed namespaces that look like ordinary async functions from the page’s perspective.

Basic IPC

1

Create a webview with HTML content

Pass an html string (or a url) to createWebview. The page does not need any special setup — wry injects window.ipc before any script runs.
import { Application } from '@webviewjs/webview';

const app = new Application();
const win = app.createBrowserWindow({ title: 'IPC Demo' });

const webview = win.createWebview({
  html: '<button id="ping">Send Ping</button>',
});
2

Listen for messages in Node

webview.onIpcMessage() fires for every message posted by the page. The message.body property is a Buffer — call .toString('utf8') to read it as text.
webview.onIpcMessage((message) => {
  console.log('Received:', message.body.toString('utf8'));
  console.log('URI:', message.uri);
});
3

Post a message from the page

Inject a script after the webview is ready, or include it in your HTML. The page calls window.ipc.postMessage(string) to send a message to Node.
webview.evaluateScript(`
  document.querySelector('#ping').addEventListener('click', () => {
    window.ipc.postMessage('hello from the page');
  });
`);
4

Start the application

app.run();
On Windows, IPC only works reliably when the page is loaded through a custom protocol (e.g. app://localhost/). Loading a file: URL on Windows will prevent window.ipc from functioning correctly.

Custom IPC Global Name

By default wry injects the IPC object as window.ipc. Pass the ipcName option to createWebview to register an additional alias on window. Both window.ipc and your custom name remain available simultaneously.
const webview = win.createWebview({
  url: 'app://localhost/index.html',
  ipcName: 'bindings',
});
The page can now use either:
window.ipc.postMessage('via default name');
window.bindings.postMessage('via custom name');
The alias is injected before any page scripts run, so it is available to inline <script> tags in the initial HTML as well as dynamically loaded modules.

Node to Page: evaluateScript and evaluateScriptWithCallback

Use evaluateScript() to push changes into the page at any time — DOM updates, data bindings, or state notifications. The call is fire-and-forget.
webview.evaluateScript(`
  document.title = 'Connected';
  document.getElementById('status').textContent = 'Ready';
`);
Use evaluateScriptWithCallback() when you need a return value. The expression result is serialized to JSON and delivered to the callback as a string.
webview.evaluateScriptWithCallback('document.title', (error, title) => {
  if (error) throw error;
  console.log('Page title:', title); // "Connected"
});

// Read a value from the page
webview.evaluateScriptWithCallback(
  'JSON.stringify({ scroll: window.scrollY, width: window.innerWidth })',
  (error, json) => {
    if (error) throw error;
    const { scroll, width } = JSON.parse(json);
    console.log(`scroll=${scroll} width=${width}`);
  },
);
Both methods accept any valid JavaScript expression or statement block. The callback variant runs asynchronously — the result arrives on the next event-loop tick after the webview evaluates it.

JSON Messages Pattern

IpcMessage.body is raw bytes. JSON is the conventional encoding for structured payloads: stringify on the page side, parse on the Node side.
// Page (in your HTML or an injected script)
window.ipc.postMessage(JSON.stringify({
  action: 'save',
  payload: { id: 42, title: 'My Document' },
}));
// Node
webview.onIpcMessage((message) => {
  const data = JSON.parse(message.body.toString('utf8'));

  switch (data.action) {
    case 'save':
      saveDocument(data.payload);
      break;
    case 'load':
      loadDocument(data.payload.id);
      break;
  }
});
If you find yourself implementing request/response correlation on top of raw JSON messages, switch to webview.expose() instead — it handles all of that automatically with full TypeScript types.

webview.expose(): Typed Promise-Based Namespaces

webview.expose(name, target) is the recommended way to bridge Node capabilities into the page. It injects a JavaScript namespace on window[name] that mirrors the shape of target:
  • Static JSON values (strings, numbers, booleans, objects, arrays) become read-only properties on the namespace.
  • Functions (including async functions) become async stubs that return a Promise on the page side.
import { readFile } from 'node:fs/promises';

webview.expose('native', {
  // Static values — available synchronously as properties
  isCool: true,
  version: '1.0.0',
  platform: process.platform,

  // Async function — returns a Promise on the page side
  readFile: async (path) => {
    return readFile(path, 'utf8');
  },

  // Regular function — also exposed as a Promise-returning stub
  add: (a, b) => a + b,
});

In-Page Usage

The page accesses static values and calls async functions through window.native:
<script>
  // Static value — read synchronously
  console.log(window.native.version); // "1.0.0"
  console.log(window.native.isCool);  // true

  // Async function — always returns a Promise
  window.native.readFile('/etc/hostname').then((content) => {
    document.getElementById('output').textContent = content;
  });

  // With async/await
  async function init() {
    const sum = await window.native.add(3, 4);
    console.log('3 + 4 =', sum); // 7
  }

  init();
</script>
The expose.mjs example in the repository combines a custom protocol for file serving with a typed native namespace:
import { readFile } from 'node:fs/promises';
import { Application } from '@webviewjs/webview';

const app = new Application();
const win = app.createBrowserWindow({ title: 'Expose Example' });

// Serve static assets via the app:// protocol
win.registerProtocol('app', async (request) => {
  // ... file-serving handler
});

const webview = win.createWebview({
  url: 'app://localhost/index.html',
  ipcName: 'bindings',
});

webview.expose('native', {
  isCool: true,
  version: '0.1.4',
  readExample: async () => readFile('./examples/assets/expose/index.html', 'utf8'),
});

app.run();

SerializationError

All values passed to expose() — both statics and function return values — must be JSON-serializable. Passing a Buffer, Date, Map, class instance, circular reference, or any value that JSON.stringify cannot handle throws a SerializationError.
import { SerializationError } from '@webviewjs/webview';

try {
  webview.expose('ns', { buf: Buffer.from('oops') }); // throws
} catch (e) {
  if (e instanceof SerializationError) {
    console.error('Value is not JSON-serializable:', e.message);
  }
}
Convert Buffer values to Base64 strings before exposing them, and Date objects to ISO strings or timestamps. The page side receives plain JSON — class prototypes are not preserved.

ExposedTarget Type Reference

type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [key: string]: JsonValue };

type ExposedTarget = Record<
  string,
  JsonValue | ((...args: any[]) => unknown | Promise<unknown>)
>;
Every top-level property of the object passed to expose() must be either a JsonValue or a callable function. Nested objects inside static values are fine as long as the whole tree is JSON-serializable.

Build docs developers (and LLMs) love