An app runs in a sandboxed iframe, so embedding another site with <iframe src="https://…"> only works for sites that permit framing — any site sending X-Frame-Options: DENY/SAMEORIGIN or a CSP frame-ancestors renders blank, and there is no client-side way around it. To build a browser / kiosk / kid-safe app that opens real sites, declare the webview capability plus a "webviewAllowlist", then open a real top-level window.
// app.json: { "capabilities": ["webview"], "webviewAllowlist": ["wikipedia.org", "khanacademy.org"] }
const { id } = await window.chatoss.webview.open({ url: 'https://en.wikipedia.org', title: 'Wikipedia' });
// …later:
await window.chatoss.webview.close(id);
The window's navigation is locked to your webviewAllowlist at the OS level (enforced in Rust): a click or redirect to any host not on the list is cancelled before it loads — a real firewall your page JS can't widen or escape. Because it's a top-level window (not an iframe), sites that refuse framing load fine. open() rejects if the URL's host isn't on the allowlist. This is the ONLY way to enforce a navigation allowlist — a JS-only allowlist inside an iframe is not a security boundary.
Embedded web views — real web pages INSIDE your app
Same capability + allowlist, but the page renders inside your own layout instead of a separate window — this is how you build in-app browser tabs, embedded dashboards, or doc panes. The easy path is mount(element, {url}), which glues a real web view to one of your DOM elements and keeps it there as the window scrolls/resizes:
// app.json: { "capabilities": ["webview"], "webviewAllowlist": ["wikipedia.org"] }
const box = document.getElementById('viewport'); // any element you sized in your layout
const view = window.chatoss.webview.mount(box, { url: 'https://en.wikipedia.org' });
await view.ready; // resolves to { id }
view.onEvent(({ url, loading }) => { /* update a URL bar / spinner / tab title */ });
await view.navigate('https://en.wikipedia.org/wiki/Cat'); // load another allowed URL
// view.close() removes it and stops tracking.
For finer control (e.g. many tabs sharing one area), use the primitives directly:
const { id } = await window.chatoss.webview.embed({ url, rect: { x, y, width, height } }); // rect = your element's box (CSS px)
await window.chatoss.webview.setBounds({ id, rect }); // call on scroll/resize to keep it glued
await window.chatoss.webview.navigate({ id, url });
await window.chatoss.webview.goBack(id); // also goForward(id), reload(id)
window.chatoss.webview.on(id, ({ url, loading }) => { /* … */ });
await window.chatoss.webview.close(id);
🔴 The embedded view is a native layer that floats ABOVE your DOM — it does not clip to rounded corners and nothing (menus, modals) can overlay it. Reserve a clear rectangle for it, and hide it (
setBoundsoffscreen, orclose) when you show UI on top. Same OS-enforced allowlist firewall asopen— an app can never point an embedded view off itswebviewAllowlist.