webContents controls and observes the page rendered inside a BrowserWindow. You don’t construct it directly - you reach it through win.webContents, and most content methods on BrowserWindow delegate straight to it. It extends Node’s EventEmitter, and on construction it bridges the native web view to ipcMain, so ipcMain.handle / ipcRenderer.invoke and webContents.send / ipcRenderer.on work with no per-window wiring.
A heads-up on scope: Bunmaska models exactly one frame per view - the top frame. Pages can still contain iframes, but the preload, the IPC bridge and dom-ready belong to the top frame only. There is no Chromium underneath, so anything that depends on the multi-frame / multi-process model (mainFrame, subframe events, RenderProcessGone, the debugger / CDP) simply isn’t here. What follows is what the class actually exposes.
Once the owning window closes, isDestroyed() returns true and every other method throws TypeError: Object has been destroyed, as in Electron.
Methods
contents.loadURL(url)
urlstring
Navigates to url. Returns void (Electron’s promise-returning form is not implemented).
import { BrowserWindow } from 'bunmaska';
const win = new BrowserWindow({ width: 800, height: 600 });
win.webContents.loadURL('https://example.com');contents.loadFile(filePath[, options])
filePathstringoptionsObject (optional)hashstring (optional) - fragment appended after#(e.g. a hash-router route).queryRecord<string, string> (optional) - query params serialized into the query string.searchstring (optional) - raw query string; takes precedence overquery.
Loads a local file. Relative paths are resolved against the current working directory and turned into a file:// URL. The path is percent-encoded, so file names containing spaces, #, or ? load correctly - which is exactly why a route fragment must go in options.hash, not inside filePath (a # in the path is now treated as a literal character in the file name, not a fragment separator).
win.webContents.loadFile('renderer/index.html');// hash-router route + query -> file:///…/index.html?tab=account#/settings
win.webContents.loadFile('renderer/index.html', {
hash: '/settings',
query: { tab: 'account' },
});A UNC path (\\server\share\…) logs a warning: the WebKit file loader ignores the file-URL host, so copy the files locally or serve them over http instead.
contents.getURL()
Returns string - the current page URL, or '' before the first navigation.
console.log(win.webContents.getURL());contents.getTitle()
Returns string - the page’s current title, or ''.
console.log(win.webContents.getTitle());contents.isLoading()
Returns boolean - whether a navigation is currently in progress. Tracked off the native navigation callbacks (did-start-loading sets it true; did-stop-loading / did-finish-load / did-fail-load clear it).
if (win.webContents.isLoading()) {
console.log('still spinning');
}contents.reload()
Reloads the current page.
win.webContents.reload();contents.reloadIgnoringCache()
Reloads the current page, bypassing the cache. Wired on all three platforms.
win.webContents.reloadIgnoringCache();contents.stop()
Stops any in-progress load.
win.webContents.stop();contents.goBack()
Navigates back one entry in the session history, if possible.
if (win.webContents.canGoBack()) {
win.webContents.goBack();
}contents.goForward()
Navigates forward one entry in the session history, if possible.
if (win.webContents.canGoForward()) {
win.webContents.goForward();
}contents.canGoBack()
Returns boolean - whether there is a previous history entry to go back to.
contents.canGoForward()
Returns boolean - whether there is a next history entry to go forward to.
contents.executeJavaScript(code)
codestring
Returns Promise<unknown> - evaluates code in the page and resolves to the script’s completion value, matching Electron’s semantics: a bare expression resolves to its value, a returned Promise resolves to its fulfilled value, and a thrown error rejects. Only JSON-serializable results survive the trip (think JSON.stringify); a result JSON cannot encode rejects right away instead of hanging. The code runs through the page’s own eval, so a page whose Content-Security-Policy lacks 'unsafe-eval' rejects it. There is no userGesture argument.
const title = await win.webContents.executeJavaScript('document.title');
const ua = await win.webContents.executeJavaScript('navigator.userAgent');
console.log(title, ua);contents.insertCSS(css)
cssstring
Returns Promise<string> - injects a <style> block into the page and resolves to a key you can later pass to removeInsertedCSS. Implemented purely through the page-world exec channel (no native CSS call), so it behaves the same on all three platforms - and, like executeJavaScript, a CSP without 'unsafe-eval' rejects it. Note: there is no options argument.
const key = await win.webContents.insertCSS('body { background: #111; color: #eee; }');contents.removeInsertedCSS(key)
keystring
Returns Promise<void> - removes a stylesheet previously added with insertCSS.
await win.webContents.removeInsertedCSS(key);contents.printToPDF()
Returns Promise<Buffer> - renders the current page to a PDF and resolves to its bytes. macOS only. On Linux it rejects with an UnsupportedPlatformError (WebKitGTK has no page-to-PDF-bytes API). On Windows it is engine-blocked: the WinCairo WebKit2 C API exposes no PDF sink (confirmed by parsing the DLL exports), so it rejects there too. Takes no options object (no page size, margins, etc. yet).
import { writeFile } from 'node:fs/promises';
const pdf = await win.webContents.printToPDF(); // macOS only
await writeFile('out.pdf', pdf);contents.capturePage()
Returns Promise<NativeImage> - captures the visible page to a NativeImage. macOS, Linux. On Windows it is engine-blocked: the WinCairo WebKit2 C API exposes no UI-process snapshot entry point (confirmed by parsing the DLL exports), so it rejects there. No rect / opts arguments.
const image = await win.webContents.capturePage(); // macOS + Linux
await writeFile('shot.png', image.toPNG());contents.setZoomFactor(factor)
factornumber -1= 100%.
Sets the page zoom factor natively. A factor that is not a finite number above 0 throws InvalidArgumentError, as Electron throws.
win.webContents.setZoomFactor(1.25);contents.getZoomFactor()
Returns number - the current zoom factor (last value set; defaults to 1).
contents.setZoomLevel(level)
levelnumber -0= 100%.
Sets zoom by level, where factor = 1.2 ** level (Electron’s relation).
win.webContents.setZoomLevel(1); // ~120%contents.getZoomLevel()
Returns number - the current zoom level (the inverse of setZoomLevel).
contents.setUserAgent(userAgent)
userAgentstring
Overrides the User-Agent string for subsequent navigations on this view. (No userAgentMetadata argument.)
win.webContents.setUserAgent('Bunmaska/1.0');contents.getUserAgent()
Returns string - the User-Agent override set via setUserAgent, or '' if none (in which case the platform default is used).
contents.sendInputEvent(event)
eventObject - a mouse or a keyboard event:- Mouse -
{ type, x, y, button? }typestring -'mouseDown','mouseUp', or'mouseMove'.xnumber - client pixels from the view content’s left edge.ynumber - client pixels from the view content’s top edge.buttonstring (optional) -'left'(default),'middle', or'right'. Ignored formouseMove.
- Keyboard -
{ type, keyCode }typestring -'keyDown','keyUp', or'char'.keyCodestring - an Electron accelerator key: a single character ('a','5') or a named key ('Enter','Escape','Tab').
- Mouse -
Synthesizes a trusted input event into the page. Unlike a script-dispatched DOM event (which the page sees as isTrusted === false), the page receives a real event with isTrusted === true - so this drives sites and widgets that reject synthetic clicks.
Windows only. On macOS and Linux it throws UnsupportedPlatformError. (On Windows the message is posted to the view’s own HWND, so it works on a hidden window and never steals the user’s focus.)
It validates before dispatch, so a bad event throws rather than firing something wrong: a TypeError on an unknown type, on a non-finite mouse x / y (so a typo can’t fire a trusted click at 0,0), or on an empty keyboard keyCode.
Coordinates are client pixels relative to the view content’s top-left, correct at 100% display scale; per-monitor DPI scaling is a follow-up.
// click at (120, 48) in the page
win.webContents.sendInputEvent({ type: 'mouseMove', x: 120, y: 48 });
win.webContents.sendInputEvent({ type: 'mouseDown', x: 120, y: 48, button: 'left' });
win.webContents.sendInputEvent({ type: 'mouseUp', x: 120, y: 48, button: 'left' });
// type a character, then press Enter
win.webContents.sendInputEvent({ type: 'char', keyCode: 'a' });
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Enter' });
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Enter' });Limits on Windows today: there are no modifier keys, so a modifiers array is ignored and Shift, Ctrl and Alt come from the physical keyboard. keyDown and keyUp know letters, digits, Enter, Tab, Escape, Backspace, Space, Delete, the arrows, Home, End, PageUp and PageDown; any other key, F1-F24 included, does nothing (char types any single character). Coordinates are only guaranteed at 100% display scaling. See the parity page for the full list.
contents.setWindowOpenHandler(handler)
handlerFunction - receives{ url }and returns{ action: 'allow' | 'deny' }.
Sets the handler consulted when the page requests a new window (window.open / target=_blank). Honest caveat: the native popup is always blocked on every platform - child-window creation isn’t supported, so { action: 'allow' } is unimplemented everywhere. Returning { action: 'allow' } logs a warning and still blocks the window, so the practical pattern is to open the URL externally and return deny. The handler’s return shape is { action } only - no overrideBrowserWindowOptions, and there is no did-create-window event. On Windows the handler is not invoked yet: setting one logs a warning, and window.open is blocked (engine work pending).
import { shell } from 'bunmaska';
win.webContents.setWindowOpenHandler(({ url }) => {
void shell.openExternal(url);
return { action: 'deny' };
});contents.openDevTools()
Opens the developer tools (web inspector) for this view. Best-effort: on macOS it relies on a private inspector SPI and logs a warning if unavailable; on Linux it uses the WebKitGTK inspector. On Windows it is stubbed (a no-op that logs a warning) - the WinCairo inspector is not wired. No options argument (no docking mode).
win.webContents.openDevTools();contents.closeDevTools()
Closes the developer tools. Best-effort. Stubbed (no-op) on Windows, like openDevTools.
contents.toggleDevTools()
Opens the devtools if closed, closes them if open (tracked via Bunmaska’s own open/closed flag).
win.webContents.toggleDevTools();contents.isDevToolsOpened()
Returns boolean - whether the devtools were last opened by Bunmaska and not since closed. This is Bunmaska’s own bookkeeping flag, not a query of the live inspector window.
contents.isDestroyed()
Returns boolean - whether the owning window has been closed/destroyed. After that, every other method throws TypeError: Object has been destroyed.
if (!win.webContents.isDestroyed()) {
win.webContents.send('tick', Date.now());
}contents.send(channel, ...args)
channelstring...argsany[]
Sends an event on channel to the renderer, where ipcRenderer.on(channel, ...) in the preload receives it. Arguments are JSON-serialized, not structured-cloned as in Electron: a Map, Set, ArrayBuffer, typed array, function, symbol or bigint throws InvalidArgumentError, a Date arrives as a string, a Node Buffer arrives as { type: 'Buffer', data: [...] }, and undefined properties vanish. Send plain data. A send before the page’s first load is queued on all platforms and flushed once the bridge is up - at dom-ready on Linux and Windows, at did-finish-load on macOS - so a message fired right after loadFile is not lost.
win.webContents.send('update-available', { version: '1.2.0' });// preload.js - ipcRenderer lives here, not in the page
import { contextBridge, ipcRenderer } from 'bunmaska/renderer';
let latest = null;
ipcRenderer.on('update-available', (_event, info) => {
latest = info;
});
contextBridge.exposeInMainWorld('updates', { latest: () => latest });// page
const info = await window.updates.latest(); // exposed functions are asyncEvents
webContents extends EventEmitter. Bunmaska emits a deliberately small, navigation-focused subset, driven by the native load callbacks on all three platforms - the navigation delegate on macOS, WebKitGTK load signals on Linux, the WebKit2 C API loader client on Windows.
Event: ‘did-start-loading’
Emitted when a load begins (the tab spinner starts).
win.webContents.on('did-start-loading', () => {
console.log('loading…');
});Event: ‘did-stop-loading’
Emitted when the load stops (the spinner stops).
Event: ‘dom-ready’
Emitted when the top frame’s document is ready (DOMContentLoaded). Iframes do not emit it.
win.webContents.on('dom-ready', () => {
void win.webContents.insertCSS('html { scroll-behavior: smooth; }');
});Event: ‘did-finish-load’
Emitted when navigation is done and the page has loaded successfully.
win.webContents.on('did-finish-load', () => {
console.log('loaded', win.webContents.getURL());
});Event: ‘did-navigate’
Returns:
eventEvent - an empty object (placeholder; nopreventDefault).urlstring - the URL navigated to.
Emitted when a main-frame navigation completes. Note the leaner payload than Electron: no httpResponseCode / httpStatusText.
win.webContents.on('did-navigate', (_event, url) => {
console.log('navigated to', url);
});Event: ‘did-fail-load’
Returns:
eventEvent - an empty object placeholder.errorCodenumbererrorDescriptionstringvalidatedURLstring - the current URL at the time of failure.
Emitted when a load fails. Error codes differ from Chromium’s, so branch on them with care:
- macOS maps the common
NSURLErrorDomaincodes to the Chromium net errors Electron reports - cancelled (-999) to-3, timed out (-1001) to-7, host not found (-1003) to-105, offline (-1009) to-106- and passes every other code through unchanged. - Linux reports WebKitGTK’s own
GErrorcode and message. - Windows reports WebKit’s own error code and description.
win.webContents.on('did-fail-load', (_event, code, description, url) => {
console.error(`load failed (${code}): ${description} @ ${url}`);
});Properties
contents.id
A number - process-unique id, matching Electron’s webContents.id. Read-only.
console.log(win.webContents.id);Not in Bunmaska (yet)
Electron’s webContents is huge; Bunmaska implements the navigation + scripting + IPC core and leaves the rest out. Notable gaps:
- Module-level statics -
webContents.getAllWebContents(),getFocusedWebContents(),fromId(),fromFrame(),fromDevToolsTargetId(). The class is only reachable viawin.webContents; there’s no registry lookup. - The frame model -
mainFrame,opener,frames, and every multi-frame event (will-frame-navigate,did-frame-navigate,did-frame-finish-load,did-start-navigation,did-navigate-in-page). One view, one frame, noWebFrameMain. - Cancellable navigation - there is no
will-navigate/will-redirect, and theeventobjects passed to the events that do fire have nopreventDefault(). - Process / lifecycle events -
render-process-gone,unresponsive/responsive,crashed,destroyed,will-prevent-unload. There’s no separate renderer process to go gone. - DevTools protocol - no
debugger(CDP), noinspectElement, nosetDevToolsWebContents. DevTools is open/close/toggle only. - Input & focus - no
before-input-event/input-event,focus()/isFocused(),beginFrameSubscription,startDrag. (sendInputEventdoes exist, on Windows - see above.) - Printing & content -
print()(onlyprintToPDF, macOS-only; engine-blocked on Windows),savePage,getPrintersAsync,findInPage/stopFindInPage. - Page dialogs -
alert(),confirm()and<input type="file">open native sheets on macOS;prompt()returnsnullthere, as it does in Electron. On Linux and Windows Bunmaska adds no handling of its own. - Editing & clipboard commands -
undo/redo/cut/copy/paste/selectAll/replace,cut-style menu wiring,replaceMisspelling. - Media / audio -
isAudioMuted/setAudioMuted,setBackgroundThrottling,getOSProcessId,getProcessId. setWindowOpenHandlerwithallow- child-window creation is unsupported, so{ action: 'allow' }is logged and ignored; there is nodid-create-window, and the handler return type omitsoverrideBrowserWindowOptions. On Windows the handler is not invoked at all yet.capturePageon Windows /printToPDFoff macOS -capturePageworks on macOS and Linux but is engine-blocked on Windows;printToPDFis macOS only (not yet wired on Linux, engine-blocked on Windows - the WinCairo WebKit2 C API exposes neither a PDF sink nor a UI-process snapshot).- Session / zoom plumbing - no
sessionproperty, nosetVisualZoomLevelLimits, nozoomLevelpersistence across reloads (zoom is stored in-memory and reapplied persetZoomFactor).