Guides

Settings and presets

Declare user-facing settings fields, reuse the shared presets, and read and react to values with dd.settings.

Settings are how users make a widget theirs. You declare fields in the manifest, the host renders them in the widget's settings dialog, and your code reads the values and reacts when they change. The division of labor is strict and worth internalizing up front: widgets read settings, the host writes them. There is no dd.settings.set; the dialog is the only writer. That is a deliberate boundary, since several field types carry user consent.

Declaring fields

Each entry in the manifest's settings array declares one field:

"settings": [
  { "key": "city", "type": "text", "label": "City", "default": "" },
  { "key": "units", "type": "select", "label": "Units", "default": "celsius", "options": ["celsius", "fahrenheit"] },
  { "key": "refreshMin", "type": "number", "label": "Refresh (minutes)", "default": 30, "min": 10, "max": 180 }
]

That is the Weather widget's real block. A quick tour of the types:

  • text, number (with optional min/max), boolean, and select (a dropdown over options; with no default the first option wins) cover most needs.
  • color gives the user a color picker. Before reaching for it, check whether the shared accentColor preset below fits; a lone custom color that ignores the theme usually looks wrong.
  • placeholder adds hint text to empty inputs, and pick adds a Browse button beside a filePath field. Both need "minAppVersion": "0.1.10".

Three types are different in kind: secretRef, filePath, and instances are consent-bearing. Their value grants access to something the user owns: a stored credential, a file on disk, other widgets. They never inherit a manifest default, only a value the user actively picked counts, and your code never sees the underlying secret or path contents directly; the value works as a key you pass back to APIs like dd.files.read or the $secret mechanism. The full field tables are in the manifest reference.

Presets: shared fields, shared behavior

Some settings recur in almost every widget, so they ship as named presets you mix into the same array:

"settings": [
  { "key": "showSeconds", "type": "boolean", "label": "Show seconds", "default": true },
  "panelStyle",
  "accentColor"
]
  • panelStyle expands to a "Panel style" select (none / solid / transparent, default solid, stored under the key panel). Put settings="panelStyle" on your dd-panel and the component applies it itself; you write zero code:

    <dd-panel id="panel" settings="panelStyle"> ... </dd-panel>
  • accentColor expands to a "Use theme accent color" toggle (key useAccent, default on) plus a "Custom color" picker (key color). This one has no component-side auto-apply; resolve it in code with dd.theme.accent(settings), or dd.theme.bindAccent(cb) to stay reactive to both settings and theme changes.

Want a preset's behavior with a different default? Declare the field inline with the same key and your default, and keep the preset name on the component; it keys off the setting value regardless of how the field was declared.

Reading values

The default idiom is dd.settings.bind: it calls your callback immediately with the current values and subscribes to changes in one step, so a change landing between a one-shot read and a later subscription cannot be missed. The Weather widget wires its whole settings story in a few lines:

let settings = {};
let poller = null;

await dd.settings.bind((next) => {
  settings = next;
  poller?.refresh();   // re-poll now with the new city and units
});

The values you receive are the effective settings: manifest defaults merged with whatever the user changed (and for consent types, only what the user picked). If you prefer the explicit pair, dd.settings.get() returns a snapshot and dd.settings.onChange(cb) subscribes; bind is just both in the right order.

Live preview

While the settings dialog is open, the host live-previews edits: your callback fires with the not-yet-saved values as the user tries them, and if they dismiss without saving, it fires once more with the stored values. Do not try to distinguish preview events from real ones; treat every callback the same and re-render from the values you were handed. That is what makes the preview feel instant, and it costs you nothing since your code path is identical. (dd.settings.get() always returns the persisted state, if you ever need it.)

Reacting to changes

What "react" means depends on the setting:

  • Values that feed a fetch: call refresh() on your dd.poll handle, as above.

  • Pure display toggles: just re-render, or bind the value with a signal and let the effect update the element.

  • panelStyle: nothing, the panel handles itself.

  • accentColor: dd.theme.bindAccent. System Controls, for example, tints its whole icon bar by re-pointing one token instead of coloring elements:

    await dd.theme.bindAccent((color) => {
      iconbarEl.style.setProperty("--dd-text", color);
    });

Reading settings needs no permission, and every widget gets the settings dialog for free; you never build the form UI yourself.