Guides

Using the components

Build widget UI from the dd-* elements, keep structure in HTML, and use signals and dd.ui.create for the parts that actually change.

The SDK registers a set of dd-* Web Components that carry the house style: dd-panel for the surface, dd-text for any text, dd-icon, dd-button, dd-icon-button, dd-chip, dd-meter, dd-app-tile, and dd-volume-pill. Reach for them before raw divs and classes: they come pre-styled from the design tokens, follow every theme for free, and keep widgets looking like one family. Attributes, parts and slots for each are in the component reference.

One behavioral fact shapes how you use them: the components are presentational. A click on a dd-button bubbles from the host element and nothing happens until you wire it; el.onclick, dd.peers.draggable(el) and friends attach to the host like to any element. The single exception is dd-volume-pill, which drives the system volume itself.

Structure stays in HTML

Someone reading index.html should be able to see what the widget is: its shape, its regions, its parts. The Clock's whole body:

<dd-panel id="panel" settings="panelStyle">
  <dd-text variant="value" id="time" data-dd-hover>--:--</dd-text>
  <dd-text variant="muted" id="date"></dd-text>
</dd-panel>
<script src="/__sdk/widget-sdk.js"></script>
<script src="widget.js"></script>

That is the whole widget, structurally. Build DOM in JavaScript only for structure that is genuinely dynamic: lists generated from data, or a region whose shape changes with state. A fixed layout with changing values is not dynamic structure; leave it in HTML and update the values.

Signals and effects

For those changing values, the SDK ships a small reactive core: dd.ui.signal, computed, and effect. Declare state as signals, bind each to its static element with one small effect, and write to .value from wherever the data arrives. The Weather widget's entire render layer:

const { signal, effect } = dd.ui;

const temp = signal("--°");
const condition = signal("");
const place = signal("");

effect(() => (tempEl.textContent = temp.value));
effect(() => (conditionEl.textContent = condition.value));
effect(() => (placeEl.textContent = place.value));

After this, "rendering" is assignment: temp.value = "21°" updates exactly the one element that reads it. computed derives values that update with their inputs; System Controls turns one vitals snapshot into per-concern slices with it:

const vitals = signal(null);
const wifi = computed(() => (vitals.value ? vitals.value.wifi : null));
const muted = computed(() => !!(vitals.value?.volume && vitals.value.volume.muted));

Do not over-reach, though. If a handful of values always change together from one callback, plain imperative updates in a render() function are fine; the Clock works that way. Signals earn their keep when many pieces change independently.

bindParts for reactive attributes

When the changing values are attributes spread through a subtree, inline SVG being the classic case, one effect per attribute gets noisy. Keep the shape in HTML, mark only the changing nodes with data-bind, and declare all the bindings in one call. The Status Bar's battery glyph:

<span id="battery">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
    <rect x="3.2" y="8.4" width="14.6" height="7.4" rx="2"/>
    <rect data-bind="level" x="4.7" y="10.2" height="3.9" fill="currentColor" stroke="none"/>
    <path data-bind="bolt" d="M11.6 9.3 9.8 12.2h3.2l-1.8 2.9"/>
  </svg>
</span>
dd.ui.bindParts(batteryEl, {
  level: {
    width: () => Math.max(0.5, (percent.value / 100) * 11.6).toFixed(1),
    opacity: () => (charging.value ? "0" : "1"),
  },
  bolt: { opacity: () => (charging.value ? "1" : "0") },
});

The fill bar narrows with charge and swaps opacity with the bolt while charging, and no code ever rebuilds the SVG. The props take the same reactive functions create() does.

create() for structure that varies

dd.ui.create builds elements, and any prop given as a function becomes a live binding. Use it for the regions whose structure genuinely varies, and for stateful art whose node count depends on data. System Controls builds its wifi button once, with every dynamic attribute bound:

const wifiBtn = create(
  "dd-icon-button",
  {
    round: true,
    active: () => activePane.value === "wifi",
    off: () => !wifi.value,
    title: () => (wifi.value ? `${wifi.value.ssid} (${wifi.value.signal}%)` : "Wifi: not connected"),
    onClick: () => flyout.toggle("wifi"),
  },
  wifiIcon(),
);

For a list, build the rows inside an explicit effect and swap them in with replaceChildren. The wifi network list from the same widget:

const netListEl = create("div", { class: "netlist dd-scroll" });

effect(() => {
  const nets = networks.value;
  if (nets === null) {
    netListEl.replaceChildren(create("div", { class: "dd-empty" }, "Scanning..."));
  } else if (nets.length === 0) {
    netListEl.replaceChildren(create("div", { class: "dd-empty" }, "No networks found"));
  } else {
    netListEl.replaceChildren(...nets.map(netRow));
  }
});

The one rule

Per subtree, pick one strategy: build once with reactive bindings, or wholesale re-render on change. Never put reactive bindings inside rows that a re-render throws away. In the list above, netRow builds plain rows with no bindings, because the effect discards them on every change; the wifi button, built once, is all bindings. Mixing the two in one subtree leaks work and confuses ownership. (The SDK auto-disposes bindings of removed elements as a backstop, but that is a backstop, not a design.)

So the division of labor across a widget reads: static shape in HTML, changing values via effect or bindParts, and only the genuinely varying region built and swapped with create.

Reaching inside a component

Component internals live in a Shadow DOM, so your CSS cannot restyle them with ordinary selectors. That is what keeps them theme-correct. When you do need to adapt one, use the exposed hooks, in escalating order: ::part() selectors, the documented custom-property knobs, named slots, and finally subclassing via dd.ui.components with a super call to keep the original behavior. All four are documented per component in the reference, with a subclass example. If a component will not expose what you need, hand-roll that one piece rather than fighting it.

/* the usual case: restyle the panel surface via its part */
#panel::part(surface) {
  flex-direction: row;
  gap: var(--dd-space-4);
}

Shared patterns worth knowing

A few house patterns ship as plain classes and helpers rather than components:

  • .dd-status: the muted "source / age / error" line pinned to a panel's bottom edge. Put dd-text elements inside; it hides itself while they are all empty.
  • .dd-empty: the centered "nothing here" placeholder, as in the list example above.
  • .dd-autoscale: the readout-scales-with-the-widget skeleton, covered in theming.
  • dd.ui.pressGuard(el): start a click handler with if (!dd.ui.pressGuard(btn)) return; to swallow double activations instead of hand-rolling a disable-and-timeout dance.