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/webviewengine. Read-only by default — a present-only view for "HTML/CSS you control", a richer alternative toTextfor 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 rendersWebViewas 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:
- Fonts are the engine's own, not the Tk theme fonts, so body text will not pixel-match the rest of the UI. Latin / Greek / Cyrillic use the bundled Go fonts; text in scripts those faces don't cover (CJK, Arabic, Hebrew, Indic, Thai, …) falls back to the operating system's installed fonts. So international HTML renders real glyphs — with correct per-script shaping, right-to-left ordering and CJK line breaking — when a covering system font is present, and as missing-glyph boxes when one is not.
- Opaque to screen readers. The rendered page is an image; assistive
technology sees no structure. For accessible body text use
Textor MarkView; reserveWebViewfor presentation that the other widgets cannot do (e.g. intricate tables).text()can seed an accessible description. - Local only.
loadUrlreads local files and the in-process VFS; it never performs network I/O. Binary assets (images) from a//zipfsbundle are limited by the text-oriented VFS bridge — prefer on-disk files ordata:URIs for images. - Forms are interactive, but local. Text fields and textareas take focus
and keyboard input (caret editing, selection, constraint validation with its
bubbles),
<input type=color|date|file>open native Tk dialogs, and withscript: truethe usual DOM events (input,change,focus,submit, …) fire. What a submitted<form>can reach is bound by the same local-only rule as everything else — there is no network to submit to. - A bounded CSS 3 subset. Frames (
<iframe>,<frameset>), media elements and web storage are not implemented, and the rendered output may still change as the engine grows. (In-page<script>is off unless you opt in — see Scripting.) The engine fails silently: what it does not support simply does not appear. To find out what a page of yours actually renders, open it inapps/pageview/main.js(the repo's page-checking tool — see Rendering gallery) rather than in a browser, which renders things this engine does not.