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 are app 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. App 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 are app 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 (app 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.

Parts the user can turn off

Most widgets have pieces some people do not want: the seconds on a clock, the vitals on a taskbar, a row of buttons. Make each one a part. Add part to the boolean that switches it, and mark the elements:

{ "key": "showClock", "type": "boolean", "label": "Clock", "default": true, "part": "clock" }
<div class="clock" data-dd-part="clock">…</div>

That is all. The SDK hides every data-dd-part="clock" element while showClock is false, live while the user tries the switch in the dialog, and the dialog lists your parts first, in a Show group. You write no class toggle and no CSS rule. App 0.4.5+.

A few details:

  • The value stays an ordinary setting. Your code reads it like any other, showIf can point at it, and values your users already saved carry over when you add part to a field you already ship.
  • A part needs no marked element. When your code shows a row only if the setting is on and there is data for it (a battery row on a desktop with no battery), keep that logic and add part just for the Show group.
  • <html> carries data-dd-parts-hidden="clock vitals" while parts are off, for CSS that closes the gap: :root[data-dd-parts-hidden~="clock"] .row.
  • Mark elements in your own document. The hiding stylesheet does not reach into a shadow root (a dd-* element itself is fine to mark), and your own display: … !important on a marked element wins over it.
  • The pins, systray and notificationCenter presets join the Show group too; their components hide themselves.

Hidden items: what users hide on their own

Users can also hide elements you never marked. Edit widget... in the widget menu (Edit taskbar... in the taskbar menu) outlines the element under the cursor, and a click hides it. The SDK builds a CSS selector for that element and hides every match from then on; the settings dialog lists the hidden items with a Show button each. You write nothing for this, but you decide how well the picks survive your next update. App 0.4.5+.

  • Give ids to the static elements a user may want gone. A unique id is the strongest anchor.
  • Load the SDK script at the end of <body>, before your own script. The SDK records the markup as written when it loads, and trusts positions in that markup only.
  • Put a data-key on the items of a list you build. Without a key, one whole item of a live list cannot be hidden (its place changes with the data), but a piece of an item still can: it hides in every item.
  • A click on an element you marked as a part turns that part off, so a part stays the better tool for a toggle you design: it has a name in the Show group and your code can react to it.

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: if a toggle only shows or hides elements, make it a part and there is nothing to react to. Otherwise 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.