How to Build a Custom Script Widget

A small JavaScript widget, written or imported in the app, running on your Home Screen — free, with no account and no catalogue of other people’s code.

Updated August 20268 min read

Every other Novique widget is built in Swift and ships with the app. A script widget is the one kind you build yourself: a JavaScript file that constructs a small widget tree — text, dates, stacks, spacers — the same way Scriptable widgets do, because Novique’s script API is a deliberate subset of Scriptable’s.

1. What a script widget is

There are exactly three script widget kinds you can place on your Home Screen — My Script 1, My Script 2 and My Script 3. You can write or import as many scripts as you like; which script each of the three slots runs is something you choose inside the app, not on the widget’s own configuration sheet.

Why three fixed slots instead of one script widget per script: iOS doesn’t reliably deliver a placed widget’s own configuration to it, so “which script does this widget run” has to live in the app’s own settings instead — which is also why a script can’t read a per-widget parameter you typed into its Edit Widget sheet.

2. Write your first script

In Novique: Create → Write a widget script → + → Write a script. Give it a name, paste this in, and tap Run to preview it before saving:

const w = new ListWidget()

const title = w.addText("Hello, Novique")
title.font = Font.boldSystemFont(18)
title.textColor = noviqueTheme.foreground()

w.addSpacer(6)

const subtitle = w.addText("My first script widget")
subtitle.font = Font.systemFont(13)
subtitle.textColor = noviqueTheme.secondary()

w.addSpacer()

Script.setWidget(w)

Script.setWidget(w) is the one line every script needs — nothing renders without it. Tap Save once the preview looks right.

The Create tab in Novique, showing Write a widget script

3. Put it on your Home Screen

Back on the Scripts screen, the Widget slots section has a picker for My Script 1, 2 and 3 — choose your new script for one of them.

Then, on your Home Screen: touch and hold an empty spot, Edit → Add Widget, search Novique, and place My Script 1 (or 2, or 3) — whichever slot you assigned it to.

4. The widget-building API

A script builds a widget imperatively — no template, no return value. These are the pieces you have:

ListWidget
The root of every script. new ListWidget() gives you a container with addText, addDate, addSpacer and addStack — the same four building blocks, nested however deep you need.
WidgetText
w.addText("…") returns one. Set .font, .textColor and .lineLimit on it.
WidgetDate
w.addDate(date) returns one that keeps itself current with no reload — call .applyTimeStyle(), .applyDateStyle(), .applyRelativeStyle(), .applyOffsetStyle() or .applyTimerStyle() to pick the format.
WidgetStack
w.addStack() nests a row or column. .layoutHorizontally() / .layoutVertically(), .spacing, .setPadding(top, leading, bottom, trailing), .cornerRadius.
Color / Font
new Color("#RRGGBB") or Color.white()/.black()/.red()/.green()/.blue()/.gray(). Font.systemFont(size), Font.mediumSystemFont(size), Font.boldSystemFont(size).
noviqueTheme
noviqueTheme.accent(), .foreground(), .secondary() and .surface() — colours that follow whichever theme pack and appearance (light/dark) the widget is drawn in. Prefer these over a literal hex.
Script.setWidget(w)
The last line of every script. Nothing renders without it.
config.widgetFamily
The placed widget's size — "small", "medium" or "large" — so one script can lay out differently per size.

5. A complete example

This is one of Novique’s own bundled scripts, unmodified — a clock with a day-progress bar. Copy it in as a starting point and change what it draws.

const now = new Date()

const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
const dateLine = WEEKDAYS[now.getDay()] + " " + now.getDate() + " " + MONTHS[now.getMonth()]

// Fraction of the day elapsed, as a 10-cell bar.
const minutesToday = now.getHours() * 60 + now.getMinutes()
const fraction = minutesToday / 1440
const filled = Math.round(fraction * 10)
const bar = "█".repeat(filled) + "░".repeat(10 - filled)

const w = new ListWidget()
w.spacing = 0

// addDate with a time style, not addText with a formatted string — a WidgetDate
// keeps itself current with no reload, which is the only way a script shows live time.
const time = w.addDate(now)
time.applyTimeStyle()
time.font = Font.boldSystemFont(40)
time.textColor = noviqueTheme.foreground()

const date = w.addText(dateLine)
date.font = Font.systemFont(13)
date.textColor = noviqueTheme.secondary()

w.addSpacer()

const meter = w.addStack()
meter.layoutHorizontally()
meter.spacing = 6

const bars = meter.addText(bar)
bars.font = Font.systemFont(11)
bars.textColor = noviqueTheme.accent()

meter.addSpacer()

const pct = meter.addText(Math.round(fraction * 100) + "%")
pct.font = Font.systemFont(11)
pct.textColor = noviqueTheme.secondary()

Script.setWidget(w)

6. Fetching live data

Request makes one GET request and awaits it — loadJSON() parses the response for you:

const ENDPOINT = "https://api.coingecko.com/api/v3/simple/price"
  + "?ids=bitcoin&vs_currencies=usd&include_24hr_change=true"

const w = new ListWidget()
w.spacing = 4

try {
  const data = await new Request(ENDPOINT).loadJSON()
  const price = data.bitcoin.usd

  const label = w.addText("Bitcoin")
  label.font = Font.systemFont(13)
  label.textColor = noviqueTheme.secondary()

  const value = w.addText("$" + Math.round(price).toLocaleString())
  value.font = Font.boldSystemFont(26)
  value.textColor = noviqueTheme.foreground()
} catch (error) {
  // A network failure states itself, rather than an empty widget that looks
  // identical to one still loading.
  const message = w.addText("Couldn't update")
  message.font = Font.systemFont(13)
  message.textColor = noviqueTheme.secondary()
}

Script.setWidget(w)

A request that hangs is cancelled after 10 seconds. Always wrap it in try/catch — a network failure should say so, not render an empty widget that looks identical to one still loading.

7. What’s off-limits

A script widget runs sandboxed, on-device, inside the same memory budget as every other Novique widget. That rules out:

  • Images Image, SFSymbol and DrawContext aren’t implemented. A script widget draws text, dates, stacks and spacers only.
  • Interactivity — no buttons, no App Intents, no tap actions inside the widget. Tapping it always opens Novique.
  • Non-GET network requests — POST, PUT and friends throw. Only http and https URLs are reachable at all.
  • Dynamic code eval, the Function constructor and WebAssembly are blocked outright — an App Store requirement for any app that runs user-supplied code, not a missing feature.
  • A custom background — a script widget always uses your theme’s own surface, like every other Novique widget. Setting backgroundColor is accepted but ignored.
  • Lock Screen and StandBy — script widgets are Home Screen only, in small, medium and large.

FAQ

Is this really free?

Yes. Script widgets are not a Pro feature — writing, importing and running scripts costs nothing, in all three slots.

Can I write more than three scripts?

Yes — write or import as many as you like in My Scripts. Only three can run on your Home Screen at once, because each is tied to a fixed widget kind (My Script 1, 2 and 3), and you choose which script each one runs.

Why can't I add my own icon or photo to a script widget?

Images aren't available to scripts yet — see "What's off-limits" above. A script widget always uses your theme's own surface, the same as every other Novique widget.

My script works in the preview but not on my Home Screen. Why?

Check Settings → Novique isn't restricted from Background Refresh, and that the script doesn't depend on args.widgetParameter — Novique has no per-widget configuration sheet, so that's always null. Assign a specific script to a slot in My Scripts instead.

Are Scriptable widgets compatible?

Many are. Novique's API is a deliberate subset of Scriptable's widget-building classes (ListWidget, WidgetText, Font, Color and friends), so a Scriptable widget that only uses those — no images, no Siri shortcuts, no notifications — often runs unmodified. Anything from Scriptable's wider API (Image, DrawContext, Keychain, Alert, and more) isn't implemented and will throw.

Novique app icon

Download Novique

Available on the App Store for iPhone and iPad.

Get the app