Conditions, lists and templates
A widget can change by itself, without the app running: switch to prepared layouts at set
times, show text in the device language, and fetch its own data. All three live in the
same WidgetConfig, and every platform resolves them right before drawing
(src/resolve.rs, with Swift, Kotlin and TypeScript ports checked against the same
fixtures in tests/fixtures/resolve/).
Conditions (when)
when on any element inside children (or on a family root) keeps the element only when
the condition holds; it is stripped before drawing. A condition reads one value by path
(the same paths as placeholders: data.…, t.…, locale, instance, loop variables):
{ "type": "text", "content": "Rain soon", "when": { "path": "data.rain", "gt": 0.5 } }
| Key | Holds when |
|---|---|
eq / ne | equal / not equal (JSON equality, numbers by value; a missing value fails both) |
gt / gte / lt / lte | both sides are numbers and compare so |
in | the value equals one of the array items |
exists | true: the value is present; false: it is missing or null |
only path | the value is truthy (not false, 0, "", [], null, missing) |
all / any / not | combine other conditions: { "any": [ {…}, {…} ] } |
Several operators in one object must all hold. A malformed condition is false.
Lists from data (repeat)
repeat turns an array into elements. It is replaced in its parent by one copy of
template per item, so it works inside any stack, grid or list:
{
"data": { "days": [{ "d": "Mon", "hi": 21 }, { "d": "Tue", "hi": 18 }] },
"small": {
"type": "vstack",
"children": [
{ "type": "repeat", "each": "data.days", "as": "day", "limit": 5,
"template": { "type": "text", "content": "{{index}}. {{day.d}} {{day.hi}}°" } }
]
}
}
as names the loop variable (default item); {{index}} counts from 0. At most 50 copies;
limit lowers that. A path that is not an array renders nothing. A loop variable hides a
built-in root of the same name (as: "t" hides {{t.…}} inside the template). A repeat
used as a family root is wrapped in a vstack.
Reusable parts (templates, use)
{
"templates": {
"row": { "type": "label", "systemName": "{{arg.icon}}", "text": "{{arg.text}}" }
},
"small": {
"type": "vstack",
"children": [
{ "type": "use", "template": "row", "args": { "icon": "sun.max", "text": "{{data.temp}}°" } },
{ "type": "use", "template": "row", "args": { "icon": "drop", "text": "{{data.hum}}%" }, "color": "#60a5fa" }
]
}
}
Arguments are read as {{arg.name}} (or with bind). An argument that is exactly one
placeholder, such as "{{data.days}}", keeps its JSON type, so a template can repeat over
it. Other keys on the use element (color, padding, …) override the template's root.
An unknown template renders nothing; templates nest up to 8 levels.
Localized strings (strings)
{{t.key}} in any string takes the value from the table for the device locale: exact tag
(pt-BR, pt_BR too; script and extension subtags are ignored, so zh-Hant-TW-u-… matches
zh-TW), then the same language, then defaultLocale, then en, then the
first table. A key missing there falls back to the default table, then shows the key.
{
"defaultLocale": "en",
"strings": {
"en": { "title": "Weather", "humidity": "Humidity" },
"ru": { "title": "Погода", "humidity": "Влажность" }
},
"small": { "type": "text", "content": "{{t.title}}" }
}
{{locale}} is the device locale itself. On the Windows Widgets Board the host reads it from
LC_ALL / LC_MESSAGES / LANG, which are often unset there — then the defaultLocale
table is used.
Newer configs on older apps (requires)
Renderers understand contract version 3 (everything on this page; version 2 had no
state / set / transition). A config that needs a
newer one says so, and an older app shows fallback instead of a half-understood layout:
{
"requires": { "resolve": 3 },
"fallback": { "small": { "type": "text", "content": "Update the app" } },
"small": { "…": "uses a future feature" }
}
Without fallback the widget shows "Update the app to show this widget". strings and
defaultLocale still apply to the fallback.