WebView

Experimental. A widget that renders a useful subset of CSS 3 / HTML 5 (over CSS 2.1, HTML 4.01, CSS Level 1 and HTML 3.2) with the pure-Go modernc.org/webview engine. Read-only by default — a present-only view for "HTML/CSS you control", a richer alternative to Text for things like complex bordered tables, floats, flexbox and positioned layout — with opt-in in-page JavaScript (see Scripting). It is still not a browser: there is no remote networking, and forms are not interactive yet. The API and the rendered output may change.

To see what the engine renders, browse the bundled Rendering gallery — a live showcase from CSS 1 through CSS 3, HTML 5, international text and interactive JavaScript.

Live preview

A bordered, zebra-striped table with status pills — the kind of self-contained HTML/CSS layout you'd reach past Text for:

Live preview. In the Player's built-in docs viewer (scriptweaver --help), this renders WebView as a live, theme-aware widget you can interact with.

The engine paints the page into an off-screen RGBA buffer that is blitted into the widget. The view reflows to the widget's real width, scrolls when the content is taller (mouse wheel and drawn scroll bars), supports mouse text selection with Ctrl+C to copy, and reports link clicks to a callback.

Beyond the CSS 1 / HTML 3.2 basics (the box model with margin collapsing, borders and backgrounds; fonts and text alignment; <table> with colspan/rowspan, captions and automatic column widths; floats) it lays out the CSS 2.1 additions — positioned layout (position: relative/absolute/fixed with z-index and overflow clipping), inline-block, min/max sizing, attribute and structural selectors, external <link rel=stylesheet> / @import stylesheets, background-image, richer list markers, and the CSS 2.1 table model (border-collapse, table-layout, border-spacing, empty-cells) — and a useful slice of CSS 3 / HTML 5: @media queries (re-evaluated on resize), flexbox (display: flex with gap and flex-grow/-shrink/-basis), 2-D transforms, custom properties (--name / var()), position: sticky, generated content on ::before/::after, the :hover/:focus pseudo-classes, border-radius, box-sizing and extended color (rgb()/hsl(), hex alpha); the HTML 5 sectioning elements (<header>, <nav>, <main>, <article>, <section>, <footer>, <figure>…), the new form-input types and data: URIs. So moderately rich, self-contained documents lay out correctly. International text is handled too (see Limitations).

Example

const wv = app.WebView({});
wv.pack.configure({ fill: 'both', expand: true });

wv.loadHtml(`
  <!doctype html>
  <html><head><style>
    body { font-family: sans-serif; color: #222; margin: 16px; }
    h1 { color: #1a5276; border-bottom: 2px solid #1a5276; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #999; padding: 4px 8px; }
    th { background: #1a5276; color: white; }
    a { color: #2e86c1; }
  </style></head>
  <body>
    <h1>Release notes</h1>
    <p>This is <b>bold</b>, <i>italic</i>, and a
       <a href="https://example.com/changelog">link</a>.</p>
    <table>
      <tr><th>Widget</th><th>Status</th></tr>
      <tr><td>Card</td><td>shipped</td></tr>
      <tr><td>WebView</td><td>experimental</td></tr>
    </table>
  </body></html>`);

// The widget never navigates on its own — you decide what a link does.
wv.onLinkClick((href) => sw.sys.openURL(href));

Methods

Method Description
loadHtml(html) Render an HTML string. Relative sub-resources (none, for an inline string) resolve against the process working directory; prefer inline styles and data: URIs here.
loadUrl(url) Render a local document: a file path, a file:// URL, or a //zipfs:/… bundle path. The document's relative images and stylesheets resolve against it. Remote URLs (http, https, …) are refused — this view never reaches the network.
onLinkClick(fn) Register a callback invoked with the link's href (a string) when the user clicks a hyperlink. Pass null to clear it. The widget itself does not follow links.
text() Return the rendered document's visible text in reading order — useful for "copy all", search, or seeding an accessible description.
enableScript() Opt this view into in-page JavaScript (see Scripting). Call it before loadHtml/loadUrl so the first document's <script>s run. Returns this; scripting is off by default.
enableExternalScripts() Additionally fetch and run external classic <script src> elements (still local-only). Off by default — real-world classic bundles assume a full browser and can degrade a page — while inline scripts and ES modules always run in script mode. Call before the load, with scripting on. Returns this.
zoomIn() / zoomOut() Step the full-page zoom up / down by a fixed ratio (×1.1 per step), clamped to the supported range. Returns this.
setZoom(factor) Set an absolute full-page zoom — 1 = 100%, 1.5 = 150%, … — clamped to [0.25, 5] (25%–500%). A non-positive or non-numeric factor is ignored. Returns this.
resetZoom() Return to 100% (factor 1). Returns this.
zoom() Return the current full-page zoom factor (1 = 100%).
setColorScheme(scheme) Select the colour scheme the page is rendered for — 'light', 'dark', or 'auto' (the default: follow the Player theme). This is what the page's @media (prefers-color-scheme: …) rules and window.matchMedia see; a loaded document re-cascades at once. Returns this.
colorScheme() Return the effective scheme, 'light' or 'dark'.

Zoom is full-page: layout and text scale together and the document reflows at the new width (it is not a bitmap stretch).

The colour scheme is the host's to say — the engine has no preference of its own — so a WebView follows the Player theme by default: a dark theme (sw.app.themesByType.dark) renders the page's prefers-color-scheme: dark variant, a light theme its light one, and a live app.setTheme() re-cascades every loaded page along with the rest of the UI. Pin a scheme with setColorScheme('light' | 'dark') (or the colorScheme option) when a page must not follow — a fixed design, a print preview; 'auto' resumes following.

Keyboard & mouse

Every WebView carries these browser interactions on the widget itself, so an app gets them for free (no wiring):

Gesture Effect
Pointer drag Select text; Ctrl+C copies the selection to the clipboard.
Mouse wheel Scroll the innermost box under the pointer (falling back to the page).
Shift + wheel Scroll horizontally.
Ctrl + wheel Zoom the page in (up) / out (down).
Ctrl with + / = / - / 0 Zoom in / in / out / reset to 100%.
Arrow keys, Home/End, PageUp/PageDown Scroll the page (when the view has keyboard focus).

Options

WebView is a display widget (a themed frame holding the rendered image). It takes the common container options:

Option Type Description
script boolean Opt into in-page JavaScript at construction — equivalent to calling enableScript() before the first load. Default false. See Scripting.
navigate boolean | string Opt into engine-driven, history-tracked navigation — equivalent to enableNavigate(). A string value pins the local-content allowlist to that URL prefix (setNavRoot). Default false.
externalScripts boolean Also run external classic <script src> elements — equivalent to enableExternalScripts(). Needs script: true to matter. Default false.
zoom number Initial full-page zoom factor (1 = 100%), applied before the first load. Clamped to [0.25, 5]; a non-positive value is ignored. Default 1.
colorScheme string 'light' or 'dark' pins the colour scheme the page is rendered for; 'auto' (the default) follows the Player theme. See setColorScheme.
width number Requested width in pixels. Usually you let geometry drive it (pack/grid/place) and the page reflows to fit.
height number Requested height in pixels.
padding string Inner padding, e.g. '8' or '8 4'.
cursor string The mouse cursor over the view.

Scripting (opt-in)

A WebView is a read-only HTML/CSS surface by default. Pass { script: true } at construction, or call enableScript() before the first load, to opt into the engine's JavaScript + DOM subsystem so in-page <script> runs: a mutable DOM with coalesced reflow, events (addEventListener, capture/bubble, preventDefault), timers and microtasks, native Promises and async/await, ES modules, and a read-only getComputedStyle. Page-side window.matchMedia answers for the current viewport and colour scheme, and window.screen reports the real display size (the widget asks Tk), not the viewport — so a page that sizes itself from screen.width behaves as it would in a browser. <canvas> works with scripting on: the 2D context — paths, gradients and patterns, text, transforms, shadows, compositing, clipping, getImageData/putImageData, toDataURL — is implemented by the engine and rasterized by the same backend that paints the page; Chart.js and uPlot render (as external classic bundles they also need externalScripts: true). Without scripting a <canvas> stays an empty box, since only a script ever draws into one.

Page scripts run on the Player's own vendored QuickJS, in a separate runtime with their own globals — they don't share your app's environment (no app/sw) and can't drive the rest of the UI. Scripting is not remote-capable either: a page's fetch / XMLHttpRequest and every sub-resource load stay local-only (files and the in-process VFS), exactly like loadUrl. With scripting off, <script> elements are ignored.

const wv = app.WebView({ script: true }); // or: app.WebView({}).enableScript()
wv.pack.configure({ fill: 'both', expand: true });
wv.loadHtml(`<button id="b">clicked 0×</button><script>
  let n = 0;
  document.getElementById('b').addEventListener('click', (e) => {
    e.target.textContent = 'clicked ' + ++n + '×';
  });
</script>`);

Limitations

This is an early, deliberately narrow widget. Known constraints:

See also