Building widgets

Theming & layout

The CSS variables, safe-area insets, layout helpers, and glow the runtime gives every widget.

The runtime injects brand colors, safe-area insets, layout helper classes, and a signature glow — so widgets look at home on the phone and adapt to light/dark without shipping their own theme.

Brand color variables

Six --preen-* variables are set on :root, resolved for the current appearance. The runtime also sets color-scheme and data-preen-scheme ("light" / "dark") on the document element.

VariableLightDark
--preen-accent#C2143A#E0435C
--preen-bg#F0EEE6#241D15
--preen-ink#1A1915#ECE8D6
--preen-muted#6B6862#A79C8C
--preen-grain#CDC1AA#3A2D1F
--preen-green#2EA043#327A3A
.value { color: var(--preen-ink); }
.label { color: var(--preen-muted); }
.accent { color: var(--preen-accent); }

Your device’s bird color

Each paired phone is given its own bird tint — the color of its bird in the Mac’s device strip (and of its “active” check in the widget list). The runtime exposes that same color to widgets as --preen-bird, so a widget can theme itself to match the device it’s running on:

/* Match this device's bird; fall back to the brand green if unset. */
.hero { color: var(--preen-bird, var(--preen-green)); }

Notes:

  • Like the brand colors, it’s resolved for the current appearance (light/dark).
  • It’s optional — always provide a fallback (var(--preen-bird, …)). It’s absent when a widget is opened standalone, and from older Mac hubs.
  • Each device starts with a tint from its position in the paired-device strip (cycling a small palette so two phones read as distinct); you can override it per device on the Mac (right-click the phone → Bird color). Either way the widget’s --preen-bird follows whatever color that phone shows on the Mac.

The built-in Tap demo uses it: its bird, button, and pulse ring all take --preen-bird, so the on-screen bird matches the one it chirps on the Mac.

Safe-area insets

The notch, calibration offset, and bottom bleed are pushed as variables — use them for padding so content clears the hardware:

VariableMeaning
--preen-safe-topClears the notch + user calibration.
--preen-safe-right / --preen-safe-leftMirrored side insets.
--preen-safe-bottomAlways 0px — content bleeds to the bottom edge.
--preen-display-bottomSymmetric chin (= top) for vertically-centered layouts.

Two helper classes apply these for you:

  • .preen-safe — clears the notch, mirrors side insets, bleeds to the bottom.
  • .preen-display — same, but adds a symmetric bottom chin so centered content sits optically centered.
  • .preen-content — a full-bleed child wrapper (100% width/height, border-box).
<body>
  <div class="preen-safe">
    <div class="preen-content"><!-- centered, notch-safe UI --></div>
  </div>
</body>

Orientation

The runtime toggles preen-landscape / preen-portrait on <body> on load, resize, and orientationchange — the CSS orientation media query is unreliable in the widget web view, so use these classes instead:

body.preen-landscape .row { flex-direction: row; }
body.preen-portrait  .row { flex-direction: column; }

The glow

Add preen-glow to <body> for the signature Preen hue — a radial wash rising from the bottom into the background. Tune it with three variables:

body {
  --preen-glow-color: #C2143A;  /* the hue */
  --preen-glow-base: #000;      /* what it fades into */
  --preen-glow-intensity: 0;    /* 0 = off … 1 = full */
}

Drive --preen-glow-intensity from your data for an at-a-glance signal (e.g. the system widget ramps it with load):

window.PREEN.onData((d) => {
  const hot = Math.max(0, Math.min(1, (d.cpu - 80) * 0.05));
  document.body.style.setProperty("--preen-glow-intensity", hot.toFixed(3));
});

The style kit

A handful of themeable building blocks ship with the runtime so widgets share one visual language instead of re-styling the same idioms. Each reads the brand tokens (--preen-accent / --preen-muted) with literal fallbacks — so override --preen-accent on <body> to re-tint a whole widget at once.

ClassWhat it is
.preen-pressThe house “jelly” tap feedback — springs down on press, bounces back. Drop it on any tappable element.
.preen-labelSmall uppercase caption (section headers, device names, on/off state). Muted; add is-on to accent it.
.preen-readoutA large live number/timer — tabular digits (no jitter as it ticks) at a light weight. Set your own font-size.
.preen-faintAmbient low-contrast text (a best-time line, idle stats).
.preen-orbThe circular hero button (toggles, push-to-talk, power). Neutral dark by default; add is-on to fill it with the accent + a matching glow. Size with --preen-orb-size; nest an <svg> glyph.
<body class="preen-glow" style="--preen-accent:#7b6cff; --preen-orb-size:148px">
  <!-- a toggle: tap-springs, fills + glows when active -->
  <div class="preen-orb preen-press is-on">
    <svg viewBox="0 0 24 24"><!-- glyph --></svg>
  </div>
  <div class="preen-label is-on">Focused</div>

  <!-- a faint, ticking count-up timer -->
  <div class="preen-readout preen-faint" style="font-size:58px">12:34</div>
</body>

Toggle is-on from JS to switch state; the orb, label, and accent all follow it:

const on = !el.classList.contains("is-on");
orb.classList.toggle("is-on", on);
label.classList.toggle("is-on", on);