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. A widget can still ask the host to open that dialog from a click, and even point at one field, with dd.settings.open().

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.
  • date and time give the user the native calendar and clock pickers. A date reads back as "YYYY-MM-DD", a time as "HH:MM", and either is "" while unset. Turn a date into a local Date with dd.dates.fromDayKey (null while unset); never pass the string to new Date(), which parses it as UTC and lands a day early west of Greenwich. Both need "minAppVersion": "0.3.4".
  • list gives the user text rows they add, reorder and remove, and your code a string[]. Use it when the widget shows one thing per entry, such as a world clock's cities: the order of the rows is the order you render. The value is always an array, [] before they add anything, and a default array seeds it. max caps the rows. Needs "minAppVersion": "0.3.7".
  • 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".
  • showIf hides a field until a sibling setting has a given value, so a dependent option only appears when it can matter (needs "minAppVersion": "0.2.6"):
{ "key": "autoCollapse", "type": "boolean", "label": "Collapse after launch",
  "default": false, "showIf": { "key": "collapsible", "equals": true } }

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, dd.image.load or the $secret mechanism. The full field tables are in the manifest reference. A bar manifest may not declare these three types at all; see the taskbars guide. In the app's widget settings dialog every secretRef dropdown carries a + button that opens Settings > Secrets with a suggested name prefilled, and the new secret selects itself when the user returns; labels don't need to explain the vault anymore.

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 / image, default solid, stored under the key panel), plus, from app 0.2.4, a "Background image" file picker (key panelImage) and an "Image fit" select (key panelImageFit, cover crops, contain letterboxes onto the surface color; nothing stretches). The two image fields show in the settings dialog only while the style sits on image. Put settings="panelStyle" on your dd-panel and the component applies all of it itself, loading the image through dd.image.load; you write zero code:

    <dd-panel id="panel" settings="panelStyle"> ... </dd-panel>

    One seam to know: the style and fit live-preview in the settings dialog, but the image itself updates on Save.

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

  • systray and pins are the bar presets (a kind: "bar" widget, app 0.3.0+): a "Show the system tray" toggle (key showTray) that <dd-tray-caret settings="systray"> applies, and a "Show pinned apps" toggle (key showPins) that <dd-apps settings="pins"> applies. See the component reference.

  • weekStart (app 0.3.4+) expands to a "Week starts on" select (monday / sunday, key weekStart). No component applies it; hand the value to dd.dates.startOfWeek, which takes the string as stored.

  • textScale (app 0.3.4+) expands to a "Text size" select (tiny / small / medium / large / huge, key textSize). Mirror the value onto the body (document.body.dataset.textScale = value) and the base stylesheet turns it into the --dd-text-scale multiplier, default 1. The autoscale skeleton rides it automatically; type sized by your own viewport units multiplies it in: calc(min(19vw, 52vh) * var(--dd-text-scale, 1)). Skip it when the type is measure-fitted with dd.ui.autofit: there the tile itself is the size knob.

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. Declare every field the preset carries, not just the one you are changing, and do not also list the preset name in the manifest: expansion appends without deduplicating, so shared fields would appear twice. Going inline also means later additions to the preset skip your widget until you copy them in.

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 = {};

await dd.settings.bind((next) => {
  settings = next;     // repaint; the refetch is dd.poll's job, see below
});

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

The Style tab next to your settings previews the same way, over theme.tokens instead: the user's per-widget token overrides stream live and revert on dismiss. There is nothing to declare; every token your CSS uses is already covered.

Reacting to changes

What "react" means depends on the setting:

  • Values that feed a fetch: name them in your dd.poll call's refreshOn, and leave the settings callback to repainting. Do NOT call refresh() from the callback: the live preview below means one change per keystroke, so that re-fetches once per character of whatever the user is typing. refreshOn waits for the values to rest and polls once.

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