Skip to content

Add a home screen widget

Checked against c1b2fa4 on 2026-11-10. The files it covers are unchanged since.

A home screen widget is a piece of your page’s markup marked as a surface. You don’t write a widget extension: the host already has a generic one that draws whatever template the page sends it. This guide follows examples/timer, a stopwatch whose widget shows the running time.

Put an element with data-niso-surface="widget.small" (or widget.medium) anywhere in the page. It isn’t drawn in the page and takes no taps, so it can sit at the bottom of your component and use the same state as everything else:

examples/timer/App.svelte
<div class="widget small" data-niso-surface="widget.small">
<div class="head"><span class="dot" class:on={running}></span><span>Stopwatch</span></div>
{#if running}
<time class="big" data-timer-start={origin}>{clock(elapsed)}</time>
{:else}
<time class="big">{clock(elapsed)}</time>
{/if}
<progress value={elapsed % 60000} max="60000" data-timer-start={running ? origin : undefined}
data-timer-period={running ? 60000 : undefined}></progress>
<p class="foot">{laps.length ? laps.length + (laps.length === 1 ? ' lap' : ' laps') : running ? 'Running' : active ? 'Paused' : 'Ready'}</p>
</div>

examples/timer/App.svelte, line 82

Give the surface the size of the widget in CSS pixels. The timer uses an iPhone’s widget sizes, and the targets scale it to fit elsewhere:

examples/timer/App.svelte
.small { width: 170px; height: 170px; display: flex; flex-direction: column; justify-content: space-between; }
.medium { width: 364px; height: 170px; display: flex; gap: 16px; }

examples/timer/App.svelte, line 169

Keep it simple: boxes with backgrounds, borders and corner radii, text, <img>, <progress> and <time>. That’s what a template holds; anything else (shadows, transforms, canvas) is dropped (What a template holds).

The widget runs no JavaScript, so it can’t update every second. Instead, a <time data-timer-start={ms}> becomes the platform’s own counting timer and a <progress data-timer-start data-timer-period> fills by itself. The template only changes when the stopwatch starts, stops or laps, and that’s when the host publishes it again (Timers that count by themselves).

Terminal window
hosts/apple/build.sh timer # iOS: the app carries NisoWidgets.appex
hosts/android/build.sh timer # Android: the app's Glance widget

Then add the widget from the home screen’s widget gallery as for any app.

  • iOS: the app and its widget extension share the App Group group.dev.niso.host (hosts/apple/Niso.entitlements). After each frame the app writes changed widget templates there (Sources/SurfacePublisher.swift) and asks WidgetKit to reload; the extension (hosts/apple/Widgets) draws them. Surfaces go nowhere on macOS, tvOS, visionOS and watchOS yet.
  • Android: the app stores the templates and the Glance widget draws widget.small or widget.medium, whichever fits the size the user gave it (SurfaceWidget.kt).

A widget shows what the page last rendered, so it’s only as fresh as the app’s last run. Updating it from a server is planned: niso_render_template can render a template headlessly, but no host takes pushed templates yet.

Widgets on iOS run no page, so buttons in a widget surface are left out. On Android they aren’t drawn in the widget either; use an activity surface for controls.