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. Putdd-textelements 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 withif (!dd.ui.pressGuard(btn)) return;to swallow double activations instead of hand-rolling a disable-and-timeout dance.