Repository navigation
Writing Widgets
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.
- Start one. Under Widgets → My widgets, click + New. Pick a template to start from, or keep the blank one.
-
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. - Save. Save (⌘S / Ctrl+S) updates the widget everywhere it's used. Save as… keeps the original and makes a new widget.
- 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.
-
Share it. Export saves a
.framedeck-widget.jsonfile. 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.
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.fetchTextinstead offetch,https://instead ofhttp://, orctx.everyinstead ofsetInterval.
Importing a widget file also runs the check and tells you how many risks it found.
The same reference is in the Reference tab of Widget Studio.
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 = '...'; },
});text, textarea, select (with options: [['value', 'Label'], ...]), toggle, datetime, place (city search, gives { label, lat, lon }), media (picture or video).
| 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 |
| 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 |
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()).
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.coloriseink6ormono) 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 withwindow.open()open in your browser.
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); }Get started
Show a screen on…
Widgets & pages
Help
Developers