Skip to content

Writing Widgets

Kai-Tung Yu edited this page Oct 30, 2026 · 3 revisions

Widgets are plain HTML, CSS and JavaScript. Widget Studio shows a live preview at several screen sizes (including a round screen and e-ink) and a console while you type.

In the app

  1. Start one. Under Widgets → My widgets, click + New. Pick a template to start from, or keep the blank one.
  2. Edit the code. The JavaScript tab describes the widget and draws it; the CSS tab styles it. Errors and console.log() show in the Console.
  3. Save. Save (⌘S / Ctrl+S) updates the widget everywhere it's used. Save as… keeps the original and makes a new widget.
  4. Change a built-in widget. Hover any widget in the list and click ✎, or select one on the screen and click Edit code. Save changes it everywhere; Reset to original undoes it. Save as… makes your own copy and leaves the built-in alone.
  5. Share it. Export saves a .framedeck-widget.json file. Others add it with Import under My widgets.

Widgets you write run in a sandbox: they can't change your layouts or files, and they can only load data from public internet addresses, not your local network.

Code check

Widget Studio checks your code every time it runs and lists findings at the top of the console. Click a line number to jump to it.

  • ⚠ Risk: something that could be misused, such as putting text from a feed or API into HTML without esc(), running code from a string (eval), adding <script> tags, or a password or API key written into the code.
  • 💡 Tip: a better way to do something, such as using ctx.fetchText instead of fetch, https:// instead of http://, or ctx.every instead of setInterval.

Importing a widget file also runs the check and tells you how many risks it found.

Reference

The same reference is in the Reference tab of Widget Studio.

How a widget works

Your code calls widget({ ... }) once. FrameDeck calls render(el, s, ctx) whenever it needs to draw: when the widget appears, when it's resized, every tick ms, and after data() loads.

widget({
  name: 'My widget', icon: '✨', desc: 'Shown in the list',
  def: [8, 4],        // default size in grid blocks
  min: [2, 2],        // smallest size
  tick: 1000,         // optional: redraw every second
  dataEvery: 60000,   // optional: how often data() reloads
  settings: [         // fields in the settings panel
    { key: 'city', label: 'City', type: 'text', default: 'Paris' },
  ],
  async data(s, ctx) { return ctx.fetchJSON('/api/...'); },
  render(el, s, ctx) { el.innerHTML = '...'; },
});

Setting types

text, textarea, select (with options: [['value', 'Label'], ...]), toggle, datetime, place (city search, gives { label, lat, lon }), media (picture or video).

ctx

Property Meaning
ctx.w, ctx.h Size in pixels
ctx.cls 'xs' | 's' | 'm' | 'l': rough size class, for switching layouts
ctx.aspect 'wide' | 'tall' | 'square'
ctx.shape 'rect' | 'round'
ctx.color 'full' | 'eink6' | 'mono'
ctx.data, ctx.error Result of data()
ctx.editor true while editing
ctx.fetchText(url) Load any public web address (no CORS problems; local network addresses are blocked)
ctx.fetchJSON(path) FrameDeck's own data (below)
ctx.every(ms, fn) A timer that stops with the widget

Built-in data

Path Data
/api/weather?lat=&lon=&units=f Current, hourly and 7-day forecast (units=c for Celsius)
/api/air?lat=&lon= Air quality
/api/system CPU, memory, uptime, host name
/api/hn Hacker News top stories
/api/rss?url= Any RSS or Atom feed
/api/ics?url=&days=14 Calendar events from an iCal link
/api/scores?league=nba Sports scores
/api/crypto?ids=bitcoin,ethereum&vs=usd Crypto prices
/api/nowplaying Spotify / Music (Mac)
/api/geocode?q= City search

Helpers

esc(text) escapes text for HTML · ring(percent, label, sub) draws a progress ring · spark(numbers) draws a sparkline · wxIcon(code), wxLabel(code) weather icons · fitText(el) shrinks text to fit · newsList(el, items, { title, per, every, ctx }) a tappable headline list · moonSvg(moonPhase()).

Styling

Use the theme colors so your widget matches every screen: var(--fg), var(--muted), var(--accent), var(--bg), var(--radius), var(--font). Size text with container units like min(10cqw, 20cqh) so it scales with the widget.

  • E-ink screens (ctx.color is eink6 or mono) show only a few colors and no animation.
  • On round screens, keep important content inside the middle; the corners are cut off.
  • console.log() shows up in the Console. Links opened with window.open() open in your browser.

Example

widget({
  name: 'Days to summer', icon: '🏖️', def: [6, 3],
  settings: [{ key: 'date', label: 'Date', type: 'datetime', default: '2027-06-21T00:00' }],
  tick: 60000,
  render(el, s, ctx) {
    const days = Math.max(0, Math.ceil((new Date(s.date) - Date.now()) / 86400000));
    el.innerHTML = `<div class="big">${days}</div>${ctx.cls === 'xs' ? '' : '<div class="sub">days to summer</div>'}`;
  },
});

CSS tab:

.big { font-size: min(30cqw, 60cqh); font-weight: 800; color: var(--accent); text-align: center; }
.sub { color: var(--muted); text-align: center; font-size: min(7cqw, 14cqh); }

Clone this wiki locally