Your first widget
A start-to-finish tutorial that builds a small clock widget: scaffold, manifest, markup, live time, and the reload loop.
This tutorial goes from nothing to a working clock on your desktop. You need DeskDash running and developer mode turned on; everything else is a text editor.
Scaffold it
Open Settings, Developer. Under New widget, enter My Clock as the
name (the folder id auto-fills as my-clock) and press Create widget.
That writes a working widget folder into your widgets directory:
my-clock/
├─ manifest.json # identity, permissions, settings, sizes
├─ index.html # the widget's document
├─ widget.js # the logic
├─ style.css # the styling
├─ jsconfig.json # editor wiring, written once, then yours
└─ .deskdash/ # generated types and schemas for your editorThe .deskdash/ folder is what makes your editor smart here: typing dd.
autocompletes the whole SDK with docs, and the manifest validates as you
type. Nothing in it runs; it exists for you.
Put it on the desktop
The new widget is already known to DeskDash. Enter edit mode from the tray
(Edit layout), open the marketplace from the edit controls and switch
to the Installed tab: My Clock is there. Add it, place it somewhere
comfortable, and leave edit mode. It says "Hello". Let's read why.
Read what you got
The scaffolded manifest:
{
"$schema": "./.deskdash/manifest.schema.json",
"id": "my-clock",
"name": "My Clock",
"version": "0.1.0",
"apiVersion": 1,
"description": "One line saying what this widget shows or does",
"author": "",
"permissions": {},
"settings": [
{ "key": "title", "type": "text", "label": "Title", "default": "" },
"panelStyle"
],
"defaultSize": { "w": 4, "h": 2 },
"minSize": { "w": 2, "h": 2 }
}permissions is empty: a fresh widget can do nothing beyond rendering.
It declares one text setting plus the shared panel-style preset, and its
sizes are in grid cells. Every field is covered in
the manifest guide.
The document is two components inside a panel, then the SDK and your script:
<body>
<dd-panel id="panel" settings="panelStyle">
<dd-text slot="heading" id="title"></dd-text>
<dd-text variant="value" id="value">--</dd-text>
</dd-panel>
<!-- The SDK first: it defines the dd-* elements and the `dd` global. -->
<script src="/__sdk/widget-sdk.js"></script>
<script src="widget.js"></script>
</body>And the script wires values to those elements with signals:
const { signal, effect } = dd.ui;
const titleEl = document.getElementById("title");
const valueEl = document.getElementById("value");
const settings = signal({});
const value = signal(null);
effect(() => {
const title = settings.value.title ?? "";
titleEl.textContent = title;
titleEl.hidden = !title;
});
effect(() => {
valueEl.textContent = value.value == null ? "--" : String(value.value);
});
async function main() {
// Nothing on `dd` is usable until the host has answered.
await dd.ready;
// Fires once now, and again whenever the user changes a setting.
await dd.settings.bind((next) => {
settings.value = next;
});
value.value = "Hello";
}
// Boot errors are reported to DeskDash rather than thrown. With developer
// mode on you will see them on the widget itself.
main().catch((err) => {
dd.log("error", "my-clock failed to start", String(err));
});Three habits in there are the house style: everything waits on dd.ready,
settings arrive through dd.settings.bind (now and on every change), and
the boot catch routes errors to the host instead of letting them vanish.
style.css is tiny and worth a look for one trick: the readout is sized
with min(16vw, 40vh), and since the widget is its own little document,
vw and vh mean the widget's own box. The number scales with the tile.
Make it tick
A clock needs the time, and the time is a capability. In manifest.json:
"permissions": { "time": true },Then in widget.js, replace the value.value = "Hello"; line with a
subscription to the host's tick stream, plus a seed so it does not sit
empty for the first second:
let lastEpochMs = 0;
function render() {
if (!lastEpochMs) return;
value.value = new Date(lastEpochMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
// inside main(), after settings.bind:
dd.time.onTick(({ epochMs }) => {
lastEpochMs = epochMs;
render();
});
const now = await dd.time.now();
lastEpochMs = now.epochMs;
render();Never build your own one-second timer; the host's tick is second-aligned and shared by every widget that needs it.
Now the loop you will live in: right-click the tray icon and pick Reload widgets. The desktop remounts your widget, permission and all, and the clock ticks. Every edit from here is save, reload, look.
Add a setting
Give users a say. In the manifest's settings array, next to the title
field:
{ "key": "showSeconds", "type": "boolean", "label": "Show seconds", "default": true },And make render() respect it:
function render() {
if (!lastEpochMs) return;
const opts = { hour: "2-digit", minute: "2-digit" };
if (settings.value.showSeconds) opts.second = "2-digit";
value.value = new Date(lastEpochMs).toLocaleTimeString([], opts);
}Also call render() from the settings.bind callback, so a change applies
instantly instead of on the next tick. Reload, enter edit mode, and open
the widget's settings from its gear: your toggle is there, rendered by the
host, and it previews live as you flip it. (After the reload, the generated
types know about showSeconds too; settings.value. autocompletes it.)
When something breaks
Break it on purpose: misspell getElementById and reload. The widget gets
a red error badge with the message, and the full line lands in the app log.
Right-click the widget and pick Inspect for the browser developer
tools; your widget is a frame you select from the console's context
dropdown. Fix the typo, reload, badge gone.
The finished widget.js
const { signal, effect } = dd.ui;
const titleEl = document.getElementById("title");
const valueEl = document.getElementById("value");
const settings = signal({});
const value = signal(null);
let lastEpochMs = 0;
effect(() => {
const title = settings.value.title ?? "";
titleEl.textContent = title;
titleEl.hidden = !title;
});
effect(() => {
valueEl.textContent = value.value == null ? "--" : String(value.value);
});
function render() {
if (!lastEpochMs) return;
const opts = { hour: "2-digit", minute: "2-digit" };
if (settings.value.showSeconds) opts.second = "2-digit";
value.value = new Date(lastEpochMs).toLocaleTimeString([], opts);
}
async function main() {
await dd.ready;
await dd.settings.bind((next) => {
settings.value = next;
render();
});
dd.time.onTick(({ epochMs }) => {
lastEpochMs = epochMs;
render();
});
const now = await dd.time.now();
lastEpochMs = now.epochMs;
render();
}
main().catch((err) => {
dd.log("error", "my-clock failed to start", String(err));
});About forty lines, and it is a real widget: permission-gated, settable, theme-following, and honest about errors.
Where to go next
- Writing the manifest, for everything the manifest can declare.
- Using the components, to build richer UI the house way.
- Working with data, when the widget should show something from the outside world.
- Interactivity, when it should respond to clicks.
- Design guidelines, to make it feel native before you publish it.
Developer mode and the dev kit
Turn on developer mode to scaffold widgets, get typed autocomplete from the generated dev kit, reload from the tray, and inspect running widgets.
Writing the manifest
What goes in manifest.json: identity, API and app version floors, the entry document, sizing on the grid, and the schema line that gives you autocompletion.