ReferenceAPI

UI toolkit

Reference for dd.ui: create, signal, computed, effect, bindParts, token, pressGuard, and the components registry.

DOM and reactive helpers. Destructure what you use at the top of widget.js:

const { create, signal, computed, effect } = dd.ui;

Two design constraints run through everything here, both deliberate:

  • Bindings are declared visibly at the create() call site: a prop or child that is a function or Signal is reactive, everything else is applied once.
  • Updates are synchronous. There is no scheduler and no batching, so nothing hides execution order.

dd.ui.create()

Builds a DOM element.

create(tag: string, props?: Props, ...children: Child[]): HTMLElement | SVGElement

Props are optional: when the second argument is a Node, string, number, array, function, or Signal, it is treated as the first child.

PropBehavior
classsets className (write class, not className)
styleobject: merged into el.style (camelCase keys); string: the style attribute
on* (onClick, onPointerDown, ...)addEventListener with the lowercased remainder; never reactive
ref: (el) => voidcalled after props and children are applied, for elements you keep a handle to
key contains - (data-*, aria-*, stroke-width), or the element is SVGattribute: true becomes "", false and nullish remove it
anything elseelement property if one exists (hidden, disabled, value), else attribute
value is a function or Signal (except on* and ref)live binding, re-applied whenever the signals it reads change

Children: strings and numbers become text nodes, arrays are flattened, null, undefined and booleans are skipped (so cond && node works), and a function or Signal child binds one text node (String(value), nullish renders as ""). Reactive children are text-only by contract; for reactive structure, write an explicit effect(() => container.replaceChildren(...)). SVG tags (svg, path, circle, rect, ...) are created in the SVG namespace automatically.

Signals

signal<T>(initial: T): Signal<T>   // { value, peek() }
computed<T>(fn: () => T): Signal<T>
effect(fn: () => void): () => void

signal(initial) returns an object with a value you read and write, plus peek() to read without subscribing the current effect. computed(fn) derives eagerly from the signals fn reads. effect(fn) runs now and again whenever any signal it read changes; it returns a dispose function, usually ignored.

Semantics, all deliberate:

  • Synchronous, no batching. A write re-runs subscribers before it returns; an effect reading N signals may run up to N times for a multi-signal update.
  • Writes are Object.is guarded. Assigning an equal value propagates nothing, so a computed that recomputes to the same result stops the chain.
  • A cascade of more than 1000 effect runs from one write throws (and logs). That is the circular-write guard.
  • No disposal lifecycle. Widgets die with their iframe, so effects simply live as long as the document. The one backstop: bindings created by create() self-dispose when their element has left the DOM.

The one rule when mixing patterns: per subtree, pick build-once with bindings, or wholesale re-render with plain create(). Never put reactive props inside rows that a render function throws away; the auto-dispose is a backstop, not a design.

dd.ui.bindParts()

Applies live props to markup that already exists: the companion to create() for a subtree whose shape is fixed but whose parts react. Stateful inline SVG is the usual case.

bindParts(root: Element, bindings: Record<string, Props>): void

Author the art in index.html, mark the changing nodes data-bind="name", then pass the same props create() takes. Reactivity, the attribute-vs-property rules, on* listeners, ref, and element-lifetime auto-dispose all behave identically.

bindParts(batteryEl, {
  level: {
    width: () => (pct.value / 100 * 11.6).toFixed(1),
    opacity: () => (charging.value ? "0" : "1"),
  },
  bolt: { opacity: () => (charging.value ? "1" : "0") },
});

root itself is eligible if it carries a data-bind. A name matching several nodes binds all of them; a name matching none logs a warning (typo insurance). Text is just a prop: { textContent: () => label.value }.

dd.ui.token()

token(name: string, fallback?: string): string

Reads a design token (a --dd-* custom property) as a string, for JS that cannot use CSS variables directly: canvas fills, inline-SVG strokes, computed layout. It reads the current value off the document root, so calling it again after a dd.theme.onChange picks up the new theme. fallback is returned when the token is unset. Unlike dd.theme.tokens(), this sees manifest font pins, so canvas text should resolve faces through it.

function readColors() {
  ctx.strokeStyle = dd.ui.token("--dd-chart-1", "#888");
}
dd.theme.onChange(() => { readColors(); redraw(); });

dd.ui.pressGuard()

pressGuard(el: Element, ms?: number): boolean

The double-activation guard for button-ish elements: returns false while el is still disabled from a previous press, otherwise disables it now and re-enables it after ms (default 500). Works on dd-* component hosts too; they reflect disabled onto their inner button. Use it at the top of a click handler:

btn.addEventListener("click", () => {
  if (!pressGuard(btn)) return;
  // ...
});

dd.ui.components

The dd-* custom element constructors, for subclassing: DdElement, DdPanel, DdText, DdIcon, DdButton, DdIconButton, DdChip, DdMeter, DdAppTile, DdVolumePill. See the component reference for the override pattern.