A widget is a single, self-contained HTML document. No build step, no bundler, no external files — everything (markup, CSS, JS) lives in one string you hand to the hub. The phone loads it fullscreen and feeds it data.
The minimum
<!doctype html>
<html lang="en">
<head><meta charset="utf-8" /></head>
<body class="perch-glow">
<div class="perch-safe">
<div class="perch-content"><!-- your UI --></div>
</div>
<script>
window.PERCH.onData((d) => render(d));
window.PERCH.ready();
</script>
</body>
</html>
Three things are doing work here:
window.PERCH.onData(cb)— your renderer; called with the latest JSON on every update. See The window.PERCH API.window.PERCH.ready()— tells the runtime the UI is hydrated.- The
perch-*classes — layout + brand helpers the runtime injects. See Theming.
The sandbox (what you can’t do)
Before your code runs, the runtime injects this Content-Security-Policy as a
<meta> tag:
default-src 'none'; connect-src 'none';
img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; font-src data:
Consequences:
- No network.
fetch,XMLHttpRequest, WebSockets, and beacon requests are blocked byconnect-src 'none'. Data only ever arrives throughonData. - No external assets. Images must be
data:URIs; fonts must bedata:or system fonts. Inline<style>and<script>are allowed. - You may add your own
<meta>CSP, but it can only further restrict — CSP policies intersect.
This is the D7 rule: a widget is a pure function of injected data, holds no token, and does no I/O. The native app does all networking on your behalf.
What the runtime provides
| Provided | What it is |
|---|---|
window.PERCH | The data + signals bridge (reference). |
--perch-* CSS variables | Brand colors + safe-area insets (theming). |
perch-safe, perch-display, perch-content, perch-glow | Layout + glow helper classes. |
perch-landscape / perch-portrait | Orientation classes toggled on <body> automatically. |
Authoring tip
You rarely write widget HTML by hand — you describe it to Claude Code and it
calls publish_widget. But knowing the contract above lets you review what it
produces and debug when a widget shows — instead of data.