Getting started

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 editor

The .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