Ensure you set
export ALLOWED_EMBEDDING_ORIGINS="https://site-that-serves-the-page-using-the-webview.example.com https://other-site.you-will-embed.browserbox-on.com"
bbx stop
bbx setup -p 8888
bbx start
cat ~/.config/dosaygo/bbpro/login.linkbefore starting the self-hosted BrowserBox instance that will be embedded.
<script src="browserbox-webview.js"></script>
<browserbox-webview
id="bbx"
login-link="https://example.com:9999/login?token=your_login_link"
embedder-origin="https://app.example.com"
ui-visible="false"
allow-user-toggle-ui="false"
width="100%"
height="600">
</browserbox-webview>const bbx = document.querySelector('browserbox-webview');
await bbx.whenReady();
// Navigate and inspect tabs
await bbx.page.navigate('https://example.com');
const tabs = await bbx.tabs.list();
const firstTab = tabs[0];
if (firstTab?.id) {
await bbx.tabs.activate(firstTab.id);
}
// Automate
await bbx.click('a.my-link');
await bbx.waitForSelector('.result');
const title = await bbx.evaluate('document.title');
// Capture
const screenshot = await bbx.capture.frame({ format: 'jpeg', quality: 80 });
// Source-frame access
await bbx.frameCapture(true);
const frame = await bbx.getFrame();
// Reset the session and navigate fresh
await bbx.cleanSlate('https://example.com');Use tab IDs from tabs.list() or the switchToTabById(targetId) / closeTabById(targetId) wrappers. Do not key tab state by positional index in an embedder.
| Attribute | Required | Default | Description |
|---|---|---|---|
login-link |
yes | — | Full BrowserBox login URL with auth token |
width |
no | "100%" |
CSS width (px if bare number) |
height |
no | "100%" |
CSS height (px if bare number) |
embedder-origin |
no | "*" |
Restrict postMessage origin |
parent-origin |
no | "*" |
Deprecated alias for embedder-origin |
request-timeout-ms |
no | 30000 |
API call timeout (ms) |
ui-visible |
no | true |
Show/hide BrowserBox chrome UI |
allow-user-toggle-ui |
no | true |
Allow user to toggle UI visibility |
interaction-mode |
no | "full" |
Policy preset: full, limited, view-only |
policy |
no | — | Additional local policy restrictions; cannot widen server policy |
chrome |
no | "default" |
Chrome presentation hint: none, minimal, default, custom |
augment-root |
no | "open" |
Augment inspectability hint: open or closed |
capture |
no | "snapshot" |
Capture policy hint: off, snapshot, sampled |
For custom host chrome, prefer ui-visible="false" and allow-user-toggle-ui="false". The legacy ui=false login-link parameter still works for backward compatibility, but the element attributes are the canonical controls.
The <browserbox-webview> element exposes a namespaced API for programmatic control.
Access via the element directly (bbx.tabs.list()) or via bbx.session facade.
| Property / Method | Returns | Description |
|---|---|---|
session.id |
string | null |
Routing machine ID |
session.usable |
boolean |
Whether the session is currently usable |
session.ready |
boolean |
Whether the API handshake has completed |
session.transport |
string |
Transport mode: "modern", "legacy", or "unknown" |
session.health() |
Promise<HealthReport> |
Probe the embedded browser and transport |
session.capabilities() |
Promise<object> |
Query supported capabilities |
session.disconnect() |
void |
Tear down the session |
session.refresh() |
void |
Reload the embedded iframe |
| Method | Returns | Description |
|---|---|---|
tabs.list() |
Promise<Tab[]> |
List all open tabs |
tabs.getActive() |
Promise<Tab> |
Get the active tab's info |
tabs.create({ url, active? }) |
Promise<Tab> |
Open a new tab |
tabs.activate(tabId) |
Promise |
Switch to a tab by ID |
tabs.close(tabId) |
Promise |
Close a tab by ID |
tabs.closeAll() |
Promise |
Close all tabs |
Tab object: { id, index, active, url, title, canGoBack, canGoForward, loading, hasFavicon, isDefaultFavicon, faviconDataURI }
| Method | Returns | Description |
|---|---|---|
page.navigate(url, opts?) |
Promise<NavResult> |
Navigate the active tab |
page.url() |
Promise<string> |
Get the active tab's URL |
page.title() |
Promise<string> |
Get the active tab's title |
page.favicon() |
Promise<string | null> |
Get the favicon as a data URI |
page.metrics() |
Promise<PageMetrics> |
Get normalized viewport/document dimensions |
page.text(opts?) |
Promise<string> |
Extract page text ({ mainContentOnly?: boolean }) |
page.reload() |
Promise |
Reload the active tab |
page.back() |
Promise<boolean> |
Navigate back |
page.forward() |
Promise<boolean> |
Navigate forward |
page.stop() |
Promise |
Stop loading |
| Method | Returns | Description |
|---|---|---|
capture.frame(opts?) |
Promise<string> |
Full-page screenshot as data URI |
capture.viewport(opts?) |
Promise<string> |
Viewport-only screenshot as data URI |
capture.enable(enabled?) |
Promise<boolean> |
Enable/disable single-slot source-frame capture |
capture.next() |
Promise<FramePacket | null> |
Consume the latest captured source frame |
Options: { format?: "jpeg" | "png", quality?: number }
FramePacket:
{
seq,
capturedAt,
frameId,
castSessionId,
sessionId,
targetId,
width,
height,
mime,
bytes: ArrayBuffer
}| Method | Returns | Description |
|---|---|---|
policy.get() |
Promise<object> |
Get current policy snapshot |
policy.get() returns the merged effective policy snapshot. Server policy is authoritative and embedder attributes can only further restrict it.
| Method | Returns | Description |
|---|---|---|
augment(spec) |
Promise<AugmentHandle> |
Create an augmentation overlay |
augment.update(id, patch) |
Promise |
Update an existing augmentation |
augment.remove(id) |
Promise |
Remove an augmentation |
augment.list() |
Promise<Augment[]> |
List all active augmentations |
Preferred content shapes:
{ type: "text", text }{ type: "html", html }{ type: "json", data }
Raw strings are still accepted as HTML shorthand.
| Method | Returns | Description |
|---|---|---|
select({ prompt, intent }) |
Promise<SelectionHandle> |
Begin interactive selection |
SelectionHandle: { getRaw(), generalize(), preview(), extract(opts?) }
getRaw()->{ selector, selectors, text, href, htmlSnippet }preview()->{ selector, matches, sample? }extract({ fields, limit })->{ selector, items, count }
Direct methods on the element for browser automation:
| Method | Returns | Description |
|---|---|---|
click(selector, opts?) |
Promise<{ result }> |
Click an element by CSS selector |
type(selector, text, opts?) |
Promise<{ result }> |
Type text into an element |
evaluate(expression, opts?) |
Promise<{ result }> |
Evaluate JavaScript in the page |
waitForSelector(selector, opts?) |
Promise<boolean> |
Wait for a selector to appear |
waitForNavigation(opts?) |
Promise |
Wait for a navigation to complete |
frameCapture(enabled?) |
Promise<boolean> |
Enable/disable single-slot source-frame capture |
getFrame() |
Promise<FramePacket | null> |
Consume the latest captured source frame |
cleanSlate(url) |
Promise<{ ok, url, targetId, sessionId, warmed, ackBlastCount }> |
Reset browser state, open a fresh tab, navigate, and warm screencast |
| Action | Example | Description |
|---|---|---|
navigate |
act({ navigate: "https://..." }) |
Navigate the active tab |
click |
act({ click: { selector: "a" } }) |
Click an element |
type |
act({ type: { selector: "input", text: "hello" } }) |
Type into an element |
evaluate |
act({ evaluate: "document.title" }) |
Evaluate JS expression |
waitForSelector |
act({ waitForSelector: { selector: "h1" } }) |
Wait for element |
waitForNavigation |
act({ waitForNavigation: {} }) |
Wait for nav |
Returns { ok, action, value }.
| Method | Returns | Description |
|---|---|---|
uiVisible(visible?) |
Promise<boolean> |
Show/hide BrowserBox chrome UI |
allowUserToggleUI(allow?) |
Promise<boolean> |
Allow/deny user UI toggling |
| Method | Description |
|---|---|
on(name, handler) |
Subscribe; returns unsubscribe function |
off(name, handler) |
Unsubscribe |
observe(config) |
Structured event observer; returns { id, config, on, off, unsubscribe } |
events(config) |
Async iterator over events |
events(config) yields { id, type, timestamp, sessionId, tabId?, detail }.
| Event | Detail | Description |
|---|---|---|
ready |
{ type } |
Legacy transport handshake completed |
api-ready |
{ methods: string[], policy?: PolicySnapshot } |
Modern API available and may include the current server policy snapshot |
ready-timeout |
{ timeoutMs, error } |
Ready handshake timed out |
tab-created |
{ index, id, url } |
New tab opened |
tab-closed |
{ index, id } |
Tab closed |
tab-updated |
{ id, url, title, faviconDataURI } |
Tab metadata updated |
active-tab-changed |
{ index, id } |
Active tab switched |
did-start-loading |
{ tabId, url } |
Page load started |
did-stop-loading |
{ tabId, url } |
Page load finished |
did-navigate |
{ tabId, url } |
Navigation committed |
favicon-changed |
{ tabId, faviconDataURI } |
Favicon updated |
policy-denied |
{ url, reason } |
Navigation blocked by policy |
policy.changed |
{ reason, policy, capabilities } |
Effective policy/capability set changed |
usability-changed |
{ usable: boolean } |
Browser usability state changed |
sos |
{ reasonCode, message, retryUrl } |
Fatal unusable signal |
disconnected |
— | Session ended |
Legacy event names continue to work. Dot-notation aliases are also emitted:
| Legacy | Canonical |
|---|---|
api-ready |
api.ready |
tab-created |
tab.created |
tab-closed |
tab.closed |
tab-updated |
tab.updated |
active-tab-changed |
tab.activated |
did-navigate |
page.navigated |
did-start-loading |
page.load.started |
did-stop-loading |
page.load.stopped |
policy-denied |
policy.denied |
policy.changed |
policy.changed |
usability-changed |
session.usability.changed |
disconnected |
session.disconnected |
Server policy is pushed to the embedder over the existing BrowserBox runtime/meta and iframe postMessage channels:
- The server emits
policySnapshotmeta when effective policy changes. - The embedded BrowserBox client caches the latest snapshot.
- The iframe bridge includes policy in the initial
bbx-api-readyhandshake. - Mid-session changes are forwarded as
bbx-policy-sync. <browserbox-webview>merges server policy with local attribute policy and emitspolicy.changedif capabilities change.
The embedder never widens server policy.
Public API failures use stable BrowserBoxError codes:
ERR_NOT_READYERR_POLICY_DENIEDERR_TIMEOUTERR_TRANSPORTERR_UNSUPPORTEDERR_INVALID_ARGUMENTERR_NOT_FOUNDERR_CONFLICTERR_INTERNAL
Errors may also include status and retriable.
All classic flat methods remain available:
whenReady(), callApi(), navigateTo(), navigateTab(), submitOmnibox(),
getTabs(), getFavicons(), getTabCount(), getActiveTabIndex(),
createTab(), createTabs(), closeTab(), closeTabById(), closeAllTabs(),
switchToTab(), switchToTabById(),
reload(), goBack(), goForward(), stop(),
getScreenMetrics(), getTransportDiagnostics(),
frameCapture(), getFrame(), cleanSlate(),
health(), refresh(), updateIframe(), stopReconnectAttempts(),
requestFrameRefresh(), reactivateActiveTab(),
listApiMethods(),
waitForNonDefaultFavicon(), waitForTabCount(), waitForTabUrl()
When a page hosting <browserbox-webview> is hidden (e.g. minimized, tab-switched) and later restored, the viewport may appear blank until a fresh frame is rendered.
Two methods handle this:
| Method | Returns | Description |
|---|---|---|
requestFrameRefresh(reason?) |
void |
Request an immediate frame from the server; no layout checks |
reactivateActiveTab(reason?) |
Promise<boolean> |
Re-activate the current tab via switchToTabById, forcing a fresh frame without any clientWidth guard |
The component already listens for visibilitychange, pageshow, and focus events internally and calls requestFrameRefresh. For embedders that wrap the webview inside custom window managers (minimize/restore), call reactivateActiveTab() explicitly on restore.
55/55 tests passing (2026-03-09) — full coverage across session, tabs, page, navigation, automation (click, waitForSelector, evaluate, act), capture, diagnostics, policy, augment, events, and observer APIs.