Building widgets

Widget anatomy

What a widget document is, what the runtime provides, and the sandbox it runs in.

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="preen-glow">
    <div class="preen-screen"><!-- device-centered UI --></div>
    <div class="preen-safe"><!-- edge-anchored, notch-safe UI --></div>
    <script>
      window.PREEN.onData((d) => render(d));
      window.PREEN.ready();
    </script>
  </body>
</html>

Three things are doing work here:

  • window.PREEN.onData(cb) — your renderer; called with the latest JSON on every update. See The window.PREEN API.
  • window.PREEN.ready() — tells the runtime the UI is hydrated.
  • The preen-* classes — layout + brand helpers the runtime injects. The two divs are stackable full-bleed layers, one per coordinate space: preen-screen spans the entire glass (center a hero element here and it’s centered on the device), preen-safe clears the notch (edge-anchored content and safe-confined backgrounds). Use one, the other, or both. 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 direct network from widget code. fetch, XMLHttpRequest, EventSource, WebSockets, and beacon requests are blocked by connect-src 'none'. Live feed data arrives through onData; datastore results arrive through the SDK’s Promises.
  • No external assets. Images must be data: URIs; fonts must be data: 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.

Inline JavaScript runs normally: rendering, timers, animation, event handlers, and local calculations all run on the phone. The boundary is direct I/O and credentials. The native app owns the network connections, while the SDK provides scoped datastore requests and consent-gated Mac actions. See Calling the Mac.

The runtime also cancels page navigation; a tapped HTTP or HTTPS link opens in the phone’s browser. The widget page itself stays in its sandbox. This web sandbox does not also sandbox scripts that you approve to run on the Mac; see the action boundary.

What the runtime provides

ProvidedWhat it is
window.PREENThe data + signals bridge (reference).
--preen-* CSS variablesBrand colors + safe-area insets (theming).
preen-screen, preen-safe, preen-display, preen-unsafe-cutout, preen-unsafe-chin, preen-content, preen-glowLayout regions + glow helper classes.
preen-landscape, preen-portraitOrientation classes, kept in sync on <html> and <body> — style per-orientation in pure CSS (.preen-landscape .row { flex-direction: row }). Never add your own resize or orientationchange listeners; the runtime handles both.
preen-relayPresent on <html> and <body> only when the phone is reaching the Mac through the Preen Plus cloud relay instead of the local network — so an “away” affordance is pure CSS (.preen-relay .badge { display: block }). Readable in JS as PREEN.viaRelay. See The window.PREEN API.
preen-hapticsPresent on <html> and <body> only when the phone can play haptics (iPhone, not iPad) — so a visual stand-in for a haptic tick is pure CSS (.preen-haptics .tick { display: none }). Readable in JS as PREEN.hapticsAvailable; fire one with PREEN.haptic(kind). See Haptics.
preen-cutout-top, preen-cutout-left, preen-cutout-right, preen-cutout-noneWhich screen edge holds the hardware cutout right now — kept in sync on <html> and <body> alongside the orientation classes (the unsafe strips re-anchor off these automatically). none marks cutout-less glass (iPad): every inset is 0, the three regions are the same full-screen rectangle, and both strips vanish.

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.

The Mac as a display

The same HTML widget can run on a Mac Notch paired to a specific display. Each display can have its own Notch and widget selection, receiving the same live data and SDK. Add optional leading, trailing, header, and body regions for a custom compact and expanded layout; see Mac top-edge surface.