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 optionalmin/max),boolean, andselect(a dropdown overoptions; with nodefaultthe first option wins) cover most needs.dateandtimegive the user the native calendar and clock pickers. Adatereads back as"YYYY-MM-DD", atimeas"HH:MM", and either is""while unset. Turn a date into a localDatewithdd.dates.fromDayKey(nullwhile unset); never pass the string tonew Date(), which parses it as UTC and lands a day early west of Greenwich. Both need"minAppVersion": "0.3.4".listgives the user text rows they add, reorder and remove, and your code astring[]. 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 adefaultarray seeds it.maxcaps the rows. Needs"minAppVersion": "0.3.7".colorgives the user a color picker. Before reaching for it, check whether the sharedaccentColorpreset below fits; a lone custom color that ignores the theme usually looks wrong.placeholderadds hint text to empty inputs, andpickadds a Browse button beside afilePathfield. Both need"minAppVersion": "0.1.10".showIfhides 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"
]-
panelStyleexpands to a "Panel style" select (none/solid/transparent/image, defaultsolid, stored under the keypanel), plus, from app 0.2.4, a "Background image" file picker (keypanelImage) and an "Image fit" select (keypanelImageFit,covercrops,containletterboxes onto the surface color; nothing stretches). The two image fields show in the settings dialog only while the style sits onimage. Putsettings="panelStyle"on yourdd-paneland 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.
-
accentColorexpands to a "Use theme accent color" toggle (keyuseAccent, default on) plus a "Custom color" picker (keycolor). This one has no component-side auto-apply; resolve it in code withdd.theme.accent(settings), ordd.theme.bindAccent(cb)to stay reactive to both settings and theme changes. -
systrayandpinsare the bar presets (akind: "bar"widget, app 0.3.0+): a "Show the system tray" toggle (keyshowTray) that<dd-tray-caret settings="systray">applies, and a "Show pinned apps" toggle (keyshowPins) that<dd-apps settings="pins">applies. See the component reference. -
weekStart(app 0.3.4+) expands to a "Week starts on" select (monday/sunday, keyweekStart). 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, keytextSize). Mirror the value onto the body (document.body.dataset.textScale = value) and the base stylesheet turns it into the--dd-text-scalemultiplier, 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.pollcall'srefreshOn, and leave the settings callback to repainting. Do NOT callrefresh()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.refreshOnwaits 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.
Writing the manifest
What goes in manifest.json: identity, API and app version floors, the entry document, sizing on the grid, and the schema line that gives you autocompletion.
Interactivity and input
How mouse input reaches a widget: the interactive flag, capture regions marked in the DOM, hover events, and what happens when widgets overlap.