# DeskDash

> DeskDash puts live widgets on your Windows desktop background and can replace the Windows taskbar with one of its own. Arrange, resize and restyle everything your way, install more from a built-in marketplace, or build your own with a documented API.

DeskDash is the Windows desktop widget app published at https://www.desktop-dashboard.com. The app is
named DeskDash and its website is desktop-dashboard.com:
one product, two names. It is also written as "Desktop Dashboard".

## At a glance

- **What it is:** a desktop application that puts live widgets on the Windows
  desktop background, behind your open windows.
- **Platforms:** Windows 10 and Windows 11, 64 bit. Mac and Linux are planned,
  not released.
- **Pricing:** a one-time purchase, no subscription and no ads. Every widget on
  the marketplace is free and included. The current amount is on https://www.desktop-dashboard.com and in
  the FAQ below.
- **Widgets:** installed from a marketplace built into the app, each declaring
  exactly what it can access before it runs. Browsable without the app at
  https://www.desktop-dashboard.com/marketplace.
- **Taskbars:** the app can replace the Windows taskbar with one from the
  marketplace (a floating dock, a side rail or a full-width bar) carrying your
  pinned and open apps, start menu and notification area. One runs at a time,
  and the Windows taskbar comes back with one click. Browsable at
  https://www.desktop-dashboard.com/marketplace/taskbars.
- **Extensible:** a widget is a folder of plain HTML, CSS and JavaScript with
  no build step, written against a documented API. Docs at https://www.desktop-dashboard.com/docs.
- **Multi monitor:** every connected display gets its own layout, remembered
  per display across reconnects.

## Key pages

- [Home](https://www.desktop-dashboard.com/): what the app does, with video.
- [About](https://www.desktop-dashboard.com/about): what DeskDash is and who builds it.
- [Download](https://www.desktop-dashboard.com/download): system requirements and how to get it.
- [Widget marketplace](https://www.desktop-dashboard.com/marketplace): every published widget.
- [Taskbar marketplace](https://www.desktop-dashboard.com/marketplace/taskbars): every published taskbar.
- [Developer docs](https://www.desktop-dashboard.com/docs): the widget API, manifest, permissions and tokens.
- [Changelog](https://www.desktop-dashboard.com/changelog): every released version.

## Frequently asked questions

### What is DeskDash?

DeskDash is a Windows app (with Linux and Mac versions coming later) that turns your desktop background into a live dashboard. You place widgets (clocks, system stats, weather, notes and more) directly on your wallpaper, arrange and style them how you like, and they stay behind your normal windows. Widgets come from a built-in marketplace, and anyone can build their own. Make your desktop be your own, with what you need in focus

### How much does it cost?

The app has a one-time cost of 12€ plus tax where it applies. There are no subscriptions, no ads and all widgets on the marketplace are free. Each license key works on one computer, and your license includes keys for two. If you need more computers, you can add keys from your account page for a small extra cost.

### Which platforms are supported?

Windows will be the first to launch. Later I plan on adding support for Mac and Linux as well.

### Does it work with more than one monitor?

Yes, multiple monitors are supported from the start. Every display gets its own layout, so you can keep widgets on one screen, run a full dashboard on another, or leave a display completely clean. Layouts remember their display and come back when you reconnect it.

### Can DeskDash replace the Windows taskbar?

Yes. The marketplace has a Taskbars section beside Widgets. Pick one and it takes the place of the Windows taskbar, with your pinned apps, open windows, start menu and notification area, in its own style: a floating dock, a side rail or a full-width bar. One taskbar runs at a time. It asks for permissions the same way a widget does, and the Windows taskbar comes back with one click in the store.

### How do I install widgets?

From inside the app: open the marketplace, pick a widget and add it to your desktop. Before it runs you see exactly what it can access, and you decide what to grant. You can browse available widgets from this site, no need to download the app.

### Will it slow down my computer?

It shouldn't be noticeable. Widgets are small web pages that only do the work you see on screen, hidden widgets are fully unloaded, and a display with no widgets costs nothing. During testing, no noticeable impact was seen. Their performance will vary though, as anyone can upload their own widgets, and some might be less optimized than others. There is also a low performance mode you can enable if you feel like your computer is struggling.

### Is it safe to run widgets I didn't write?

Yes (mostly). People will be able to submit their own widgets and I'll try to keep approval and verification process at a high level. Security is a very important aspect and I'd like to make sure people can feel safe downloading from the marketplace.

### Can I build my own widgets?

Yes, the app offers a Developer mode that gives you access to widget inspecting, and generating your own starting template. Docs are available on this site and you can check them to see how to build a good widget. Files for AI agents are also available if you want to go that route.

# Is DeskDash safe? How every widget is sandboxed and asks before it runs (/blog/is-deskdash-safe)

> Is DeskDash safe? Every widget runs in a sandbox, reaches nothing on its own, and store widgets ask before they run and pass a human code review.

## In short

- DeskDash runs every widget in a sandboxed frame that cannot reach the app, your files, the network or other widgets on its own. Anything more goes through the app, which checks every call.
- Before a widget from the store runs, you see every permission it asks for in plain language, including the exact web hosts it may talk to, and you can withhold any of them. An update that asks for more asks you again.
- Every store submission passes automated checks, then a person reads all of its code; minified or obfuscated code is refused, and an accepted version cannot be swapped afterwards.
- API keys and passwords stay in Windows Credential Manager and never enter a widget. The app itself sends only update, licence and marketplace requests, with no analytics or tracking.

DeskDash runs every widget in a [sandboxed frame](https://www.desktop-dashboard.com/docs/concepts/security#the-frame), a web page that cannot see the app around it. On its own a widget [reaches nothing](https://www.desktop-dashboard.com/docs/concepts/security#the-network), the network included. Before a widget you install from the store runs, you see [every permission it asks for](https://www.desktop-dashboard.com/docs/concepts/security#install-consent-narrows-the-grant) and decide what to grant. Every store submission passes automated checks, then [a person reads the code](https://www.desktop-dashboard.com/docs/guides/publishing#review).

The app is [built as if every widget might be hostile](https://www.desktop-dashboard.com/docs/concepts/security), because what users rely on has to hold for the worst widget on the marketplace, not the average one. In the [homepage FAQ](https://www.desktop-dashboard.com), the founder's answer to "Is it safe to run widgets I didn't write?" is a commitment to keep the verification process at a high level.

The DeskDash here is the Windows widget and taskbar app [from this site](https://www.desktop-dashboard.com/about), desktop-dashboard.com. A separate product that overlays calendars and stock tickers on a wallpaper also uses the name DeskDash, and nothing on its page connects it to this one.

## Who checks a widget before it reaches the store? [#who-checks-a-widget-before-it-reaches-the-store]

Every submission goes through [automated checks and then a human review of the actual code](https://www.desktop-dashboard.com/docs/guides/publishing#review) before any listing appears.

The automated part is strict. Upload validation enforces the manifest rules (the manifest is the file where a widget declares what it is and what it wants), and [unknown fields are errors](https://www.desktop-dashboard.com/docs/guides/publishing#what-validation-checks).

The human part depends on readable code. The reviewer reads every file, so [no minified or obfuscated code](https://www.desktop-dashboard.com/docs/guides/publishing#review) is accepted, and widgets ship as plain source with no build step. Permissions must be [justified by what the widget visibly does](https://www.desktop-dashboard.com/docs/guides/publishing#review), and web hosts and link prefixes get particular attention.

Authors are not anonymous. Publishing needs a [purchased DeskDash account](https://www.desktop-dashboard.com/docs/guides/publishing), and the publisher handle becomes part of every widget id.

Once accepted, a version is fixed. Its [exact bytes are what users install](https://www.desktop-dashboard.com/docs/guides/publishing#after-acceptance), verified against a checksum, and no one, the author included, can replace them.

## What does DeskDash ask before a widget runs? [#what-does-deskdash-ask-before-a-widget-runs]

Before a widget you install from the store runs, DeskDash shows [every requested permission in plain language](https://www.desktop-dashboard.com/docs/concepts/security#install-consent-narrows-the-grant), with each web host and link type spelled out item by item, and you can withhold any of them. Widgets that come with the app are installed alongside it and run with the [permissions their listing and manifest declare](https://www.desktop-dashboard.com/privacy).

![The DeskDash install prompt for the Weather widget: it asks for internet access, limited to api.open-meteo.com and geocoding-api.open-meteo.com, and for storage inside the dashboard, with an option to exclude permissions before installing.](/blog/deskdash-permission-prompt.png)

The starting point is nothing. [Everything is denied by default](https://www.desktop-dashboard.com/docs/reference/permissions), and you consent at install time, permission by permission. The [wording in the permission table](https://www.desktop-dashboard.com/docs/reference/permissions#the-permissions) is exactly what the marketplace shows at install. Four examples:

| Permission                                                                             | Shown at install as                                                                      |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [files](https://www.desktop-dashboard.com/docs/reference/permissions#the-permissions)  | Reads files or folders you explicitly pick for it                                        |
| [system](https://www.desktop-dashboard.com/docs/reference/permissions#the-permissions) | Reads system stats like CPU, memory and battery                                          |
| [power](https://www.desktop-dashboard.com/docs/reference/permissions#the-permissions)  | Shuts down, restarts (also into advanced startup), sleeps, locks or signs out of this PC |
| [games](https://www.desktop-dashboard.com/docs/reference/permissions#the-permissions)  | Sees the games installed on this PC and can start them                                   |

Two lines on the prompt state exactly what a grant allows, which is why the list is shown. "Talks to the internet, only to:" followed by hostnames means the widget can send requests to [those exact hosts and no others](https://www.desktop-dashboard.com/docs/reference/permissions#http), with no wildcards and every redirect hop re-checked. "Can open these link types on your PC:" means [only links that start with those prefixes](https://www.desktop-dashboard.com/docs/reference/permissions#links), and dangerous schemes are refused whatever a manifest declares.

Reading never implies acting. System stats and system controls are [separate grants](https://www.desktop-dashboard.com/docs/reference/permissions#read-and-control-pairs), and power is its own permission, so granting a volume slider is not granting the ability to turn the machine off.

What you withhold stays withheld. A denied capability [behaves exactly as if the manifest never declared it](https://www.desktop-dashboard.com/docs/concepts/security#install-consent-narrows-the-grant). The grant [only ever narrows](https://www.desktop-dashboard.com/docs/concepts/security#install-consent-narrows-the-grant), and an update that asks for something new asks you again, for exactly the additions.

## What can a widget reach while it runs? [#what-can-a-widget-reach-while-it-runs]

A running widget is a web page in a [sandboxed frame](https://www.desktop-dashboard.com/docs/concepts/security#the-frame) that can compute and render, and every reach beyond that goes [through the app, which checks it](https://www.desktop-dashboard.com/docs/concepts/security#identity-and-the-bridge).

The frame allows scripts and nothing else, and it gets an [opaque origin](https://www.desktop-dashboard.com/docs/concepts/security#the-frame): in plain terms, a page that cannot reach into the surrounding app and shares no cookies or storage with anything. The app's internal command channel is unusable from widget frames, so there is [no path from widget JavaScript to the host application, the filesystem or the OS](https://www.desktop-dashboard.com/docs/concepts/security#the-frame).

The network is the load-bearing line. Every response served to a widget carries a content security policy, the browser rule that tells a page what it may load and connect to, and its connection rule is [`connect-src 'none'`](https://www.desktop-dashboard.com/docs/concepts/security#the-network): the page cannot open a connection of any kind, from any widget, ever. Under the same policy, scripts load [only from the widget's own folder](https://www.desktop-dashboard.com/docs/concepts/security#the-network), so there is no remote code, no inline script and no nested frames.

Data still gets in, by two routes, and both [run through the app](https://www.desktop-dashboard.com/docs/concepts/security#the-network): a web fetch against the exact hostnames the manifest declared and you consented to, re-validated on every redirect hop, and a feed fetch at an address you typed into a setting, which returns only the parsed items.

Identity is not something a widget states. The app knows who is asking [from which frame sent the message](https://www.desktop-dashboard.com/docs/concepts/security#identity-and-the-bridge), never from anything inside it, so a widget cannot claim to be another widget. Every call is re-validated inside the app, and the SDK (the toolkit widgets call) is [convenience, not enforcement](https://www.desktop-dashboard.com/docs/concepts/security#identity-and-the-bridge), so nothing a widget does in JavaScript can skip the checks.

## Can a widget read my files, my passwords or my API keys? [#can-a-widget-read-my-files-my-passwords-or-my-api-keys]

A widget reads only the file [you picked for it in its settings](https://www.desktop-dashboard.com/docs/concepts/security#user-granted-never-widget-chosen), and the value of a secret never enters the widget at all.

The rule the docs apply to every sensitive capability is short: [the widget names things, the user supplies them, the host resolves them](https://www.desktop-dashboard.com/docs/concepts/security#user-granted-never-widget-chosen).

API keys and passwords. A secret setting names a credential; you pick which stored secret it means, the app injects the value into the outgoing request, and [the value itself never enters widget code, logs or error messages](https://www.desktop-dashboard.com/docs/concepts/security#user-granted-never-widget-chosen). API keys live in Windows Credential Manager, [never in widgets](https://www.desktop-dashboard.com).

Files. A file setting is a [path only you can fill in](https://www.desktop-dashboard.com/docs/concepts/security#user-granted-never-widget-chosen), and manifest defaults for it are ignored, so no widget can ship pre-pointed at anyone's files. A file read returns the file you configured in that setting, [capped at 1 MB](https://www.desktop-dashboard.com/docs/reference/api/settings-storage#ddfiles).

Desktop and folder items. A widget granted desktop or folder access sees what is on the desktop or in the folder you pointed it at, and it can [open an item only by the id the app issued](https://www.desktop-dashboard.com/docs/reference/api/desktop-apps#dddesktoplist), never by a path. Items you drop on a widget reach it as [an id, a name, a kind and an icon only](https://www.desktop-dashboard.com/changelog), and [nothing the widget says can add an item](https://www.desktop-dashboard.com/docs/reference/api/desktop-apps#ddlaunchers).

Games and wifi follow the pattern. A games widget gets opaque ids, meaning tokens with no information inside them, and [the appid, the registry id and the install path never cross](https://www.desktop-dashboard.com/docs/reference/api/desktop-apps#ddgames). Wifi connect works only for networks Windows already has a profile for, and [passwords never cross the bridge](https://www.desktop-dashboard.com/docs/concepts/security#user-granted-never-widget-chosen).

## Can widgets see each other, or my open windows? [#can-widgets-see-each-other-or-my-open-windows]

Widgets are [mutually invisible](https://www.desktop-dashboard.com/docs/concepts/security#between-widgets), and only a taskbar with the taskbar grant receives your open windows; a [desktop widget never does](https://www.desktop-dashboard.com/docs/concepts/security#the-taskbar-surface).

There is [no widget-to-widget channel, no shared storage, and no way to enumerate what else is running](https://www.desktop-dashboard.com/docs/concepts/security#between-widgets). The two exceptions run through you: one widget can control another only on the instances you ticked in its settings, and there is deliberately [no way for a widget to write its own settings](https://www.desktop-dashboard.com/docs/concepts/security#between-widgets), because that would let widgets approve themselves.

Taskbars are the one kind that sees your windows, because showing them is their job. The taskbar surface is [refused for every other kind before permissions are even read](https://www.desktop-dashboard.com/docs/concepts/security#the-taskbar-surface). A taskbar with the grant receives the [list of open windows with their titles](https://www.desktop-dashboard.com/docs/reference/api/bar-widgets#ddwindowslist) for its previews and labels. Launching, pinning, closing and tray clicks run inside the app, and what comes back is [a count or an open state, never an icon, a handle or a menu](https://www.desktop-dashboard.com/docs/concepts/security#the-taskbar-surface).

## What happens when a widget misbehaves? [#what-happens-when-a-widget-misbehaves]

A misbehaving widget [only affects itself](https://www.desktop-dashboard.com); the desktop and every other widget keep running.

A failed call returns a [typed error to the widget](https://www.desktop-dashboard.com/docs/concepts/security#identity-and-the-bridge) and nothing else. A hidden widget [unmounts entirely](https://www.desktop-dashboard.com/docs/concepts/security#between-widgets), so nothing invisible keeps running or listening. If a stored grant record is corrupted, it [fails closed to deny-all](https://www.desktop-dashboard.com/docs/concepts/security#install-consent-narrows-the-grant), and no stored state can escalate past the manifest.

The app protects its own updates the same way: application updates are [cryptographically signed and verified](https://www.desktop-dashboard.com/privacy) before they can be installed.

A way around any of this counts as a bug. The security docs say any other door is [worth reporting](https://www.desktop-dashboard.com/docs/concepts/security#what-this-means-for-you) to [contact@desktop-dashboard.com](mailto:contact@desktop-dashboard.com). The privacy policy states that no system is completely secure and asks for a [report before public disclosure](https://www.desktop-dashboard.com/privacy).

## What does DeskDash send off my PC? [#what-does-deskdash-send-off-my-pc]

The app talks to its servers for [three things only](https://www.desktop-dashboard.com/privacy), checking for updates, validating your licence and browsing or downloading from the marketplace, and it contains no analytics, tracking or crash reporting.

The policy calls its request table [the complete list](https://www.desktop-dashboard.com/privacy):

| Request                                                           | What it carries                                          |
| ----------------------------------------------------------------- | -------------------------------------------------------- |
| [Update check](https://www.desktop-dashboard.com/privacy)         | Nothing identifying                                      |
| [Licence validation](https://www.desktop-dashboard.com/privacy)   | Licence key, hashed device identifier, two widget counts |
| [Marketplace browsing](https://www.desktop-dashboard.com/privacy) | Anonymous, session token held in memory                  |

The two counts are how many widgets you have set up and how many are on screen. The servers [never receive](https://www.desktop-dashboard.com/privacy) your layout, your settings, the contents of any widget, or anything about what you do on your computer, those two counts excepted.

A widget's own connection to an outside service goes [directly from your computer to that service](https://www.desktop-dashboard.com/privacy) and not through DeskDash; the review and the sandbox constrain what a widget can reach, but they do not control what a third-party service does with data it receives. That is why the install prompt names every host: a widget's web requests go to [those exact hosts, with no wildcards](https://www.desktop-dashboard.com/docs/reference/permissions#http).

## What can a desktop widget reach in general? [#what-can-a-desktop-widget-reach-in-general]

What an add-on can reach depends on the app's model, and each classic model documents it: a Windows 7 gadget runs with all the permissions of your login account, and a Rainmeter skin can run any external program.

Windows 7 gadgets. Microsoft's archived gadget security page says, under Full Trust, that after the user accepts the warning, the gadget [runs with all of the permissions associated with the user's login account](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/-sidebar-gadget-security). Gadgets are [not limited by the standard browser security model](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/-sidebar-gadget-security), and in aggregate they have the same functionality as other code.

Rainmeter skins. The manual's RunCommand plugin [executes an external command or program in Windows](https://docs.rainmeter.net/manual/plugins/runcommand/), allowing the skin to obtain and use any output from the program. Its State option lists [Hide as the default](https://docs.rainmeter.net/manual/plugins/runcommand/), and the page describes no per-skin permission prompt.

Packaged Store apps. Microsoft's capability reference lists [runFullTrust](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/app-capability-declarations) as the restricted capability a package needs when it uses features that need full trust.

Each model states what its add-ons can reach, and the line is yours to draw. The [Rainmeter alternatives guide](https://www.desktop-dashboard.com/blog/rainmeter-alternatives) goes through what each current tool's widgets can reach on your PC, and the [DeskDash vs Rainmeter comparison](https://www.desktop-dashboard.com/blog/deskdash-vs-rainmeter) puts two of these models side by side.

## Frequently asked

### Can a DeskDash widget read my files?

Only the file you picked in a file setting, [capped at 1 MB](https://www.desktop-dashboard.com/docs/reference/api/settings-storage#ddfiles). A desktop or folder widget sees the items in the place you pointed it at and opens them only through the app.

### Can a widget see my API keys or passwords?

No. The app injects a secret's value into an approved request, and the value [never reaches widget code](https://www.desktop-dashboard.com/docs/reference/api/settings-storage#how-secrets-work), logs or error messages. Wifi passwords never cross the bridge either.

### Can a widget access the internet on its own?

No. The content security policy [forbids every connection from the frame](https://www.desktop-dashboard.com/docs/concepts/security#the-network). The only route is through the app, to the exact hosts you approved at install.

### Who reviews widgets in the store?

Automated validation runs first, then [a person reads every file](https://www.desktop-dashboard.com/docs/guides/publishing#review) of plain, unminified source. Every author holds a purchased account.

### Where do I download DeskDash?

From the [official download page](https://www.desktop-dashboard.com/download) on desktop-dashboard.com: the installer downloads straight after checkout, and the app updates itself from then on, with every update signed and verified before it installs.

# DeskDash vs Seelen UI: a whole desktop suite, or widgets and a taskbar (/blog/deskdash-vs-seelen-ui)

> DeskDash vs Seelen UI compared: what each changes on Windows, where widgets come from, what a widget can reach, taskbars, multi-monitor and price.

## In short

- DeskDash and Seelen UI both run on top of Windows, put live widgets on the wallpaper and can replace the Windows taskbar. DeskDash adds widgets and a taskbar, and nothing else changes. Seelen UI is an open source desktop suite that adds a toolbar, dock, tiling window manager, launcher, virtual desktops and widgets.
- A DeskDash widget runs in an isolated frame and asks for each capability before it runs, and you see the request first. A Seelen UI widget calls the app's commands directly through its JavaScript API, and the docs describe no grant step; its Store package declares runFullTrust.
- DeskDash is a one-time purchase, and every widget and taskbar in its marketplace is free, community ones included. Seelen UI is free under AGPL-3.0, with a Seelen Sky subscription for cloud sync and subscriber-only widgets.
- Widgets that act, plus a taskbar of your choice from one store, and nothing else changed: that is DeskDash. A tiling window manager, virtual desktops or a launcher: that is Seelen UI.

**DeskDash** lets you design a dashboard and a taskbar on the Windows desktop in your own style, using [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) widgets from an open, growing store. **Seelen UI** is an [open source desktop environment](https://github.com/eythaann/Seelen-UI) that gives Windows a macOS-like look: toolbar, dock, tiling window manager, launcher, virtual desktops and widgets, all curated for that specific look. Both put widgets on the wallpaper and both can replace the taskbar.

On screen you see the same kind of thing in both. The difference is what each one is, where its widgets come from, what a widget may reach on your PC, and how you get and configure them. Neither one replaces Windows; both run on top of it. **DeskDash** adds two things, widgets and an optional taskbar: you find a widget in the app, install it, grant what it may access, update it and remove it in the same place, and the Windows taskbar comes back with one click. **Seelen UI** ships a whole suite, opt-in piece by piece from its settings app. The [Rainmeter alternatives guide](https://www.desktop-dashboard.com/blog/rainmeter-alternatives) puts both beside the other maintained widget tools.

## DeskDash vs Seelen UI at a glance [#deskdash-vs-seelen-ui-at-a-glance]

| Axis                               | DeskDash                                                                                                                                                                    | Seelen UI                                                                                                     |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| What it is                         | Widgets plus an optional taskbar, added to Windows                                                                                                                          | Whole desktop suite layered over Windows                                                                      |
| Licence and price                  | One-time purchase, every widget and taskbar free                                                                                                                            | Free, AGPL-3.0; Seelen Sky subscription for extras                                                            |
| Where widgets come from            | In-app free marketplace, first-party and community                                                                                                                          | Built-in set plus resources on seelen.io                                                                      |
| Get, update, remove                | Browse, Installed and Updates tabs in the app                                                                                                                               | Click a resource on the website; app updates by channel                                                       |
| What a widget can reach            | [Isolated frame](https://www.desktop-dashboard.com/docs/concepts/security); capabilities [granted per widget](https://www.desktop-dashboard.com/docs/reference/permissions) | App commands through a [typed invoke wrapper](https://seelen.io/docs/widget-js-api), no grant step documented |
| Authoring                          | Plain HTML, CSS, JavaScript; reviewed before listing                                                                                                                        | Built web app bundled to YAML, uploaded on the site                                                           |
| Taskbar                            | Marketplace custom taskbars; Windows bar back in one click                                                                                                                  | SeelenWeg dock, full-width or floating                                                                        |
| Tiling, virtual desktops, launcher | No                                                                                                                                                                          | Yes                                                                                                           |
| Theming                            | Wallpaper-derived theme, theme builder, per-widget Style tab                                                                                                                | CSS or SCSS themes, stackable                                                                                 |
| Multi-monitor                      | Own layout per display, per-display widget size                                                                                                                             | Dock and toolbar per monitor                                                                                  |
| Install                            | Installer from the website; auto-updates                                                                                                                                    | Store, winget, .msix or .exe; needs WebView2                                                                  |

## What each one changes about your desktop [#what-each-one-changes-about-your-desktop]

**DeskDash** adds widgets and an optional taskbar to Windows and changes nothing else; **Seelen UI** adds a whole suite over Windows, from a toolbar and a dock to a tiling window manager and virtual desktops.

**DeskDash** widgets sit on the wallpaper behind your windows, and a double-click on an empty spot of the desktop hides them all and brings them back. The widgets act and keep their state: [Shelf](https://www.desktop-dashboard.com/marketplace/deskdash.shelf) takes a file, folder or program dropped from Explorer and launches it from a tile, Desktop Drawer turns the desktop or any folder into an app drawer, Todo and Habits are lists you tick off, and Now Playing carries transport controls. The [marketplace](https://www.desktop-dashboard.com/marketplace) carries the rest, clock and weather included. Explorer, the desktop and window management stay as Windows made them.

![A DeskDash desktop on a sunset wallpaper: a status bar across the top with the weather, the date, the time and the volume, the day of the week set large, a Slot Machine widget, a Shelf of game shortcuts on the left, a Steam Recent strip of recently played games at the bottom, an empty Tasks list on the right, and the Pulseglass Dock taskbar along the bottom edge with the playing track, CPU, RAM and GPU readings and the clock.](/blog/deskdash-desktop-widgets-taskbar.png)

**Seelen UI**'s [README](https://github.com/eythaann/Seelen-UI) groups its parts in three blocks: a custom toolbar, a custom dock, desktop widgets, flyouts and a start menu replacement; a tiling window manager with a custom Alt+Tab, per-app rules, floating mode and gaps; and workspaces with custom virtual desktops, a wallpaper per workspace and named workspaces. The [product page](https://seelen.io/apps/seelen-ui) adds video wallpapers and interactive HTML wallpapers.

## Where do the widgets and taskbars come from, and how do you get them? [#where-do-the-widgets-and-taskbars-come-from-and-how-do-you-get-them]

**DeskDash** widgets and taskbars come from a marketplace inside the app, where you install, grant, update and remove them; **Seelen UI** has a built-in set of widgets and installs community resources you pick on its website.

The **DeskDash** marketplace has a Widgets section and a Taskbars section, each with Installed, Browse and Updates tabs. You pick a widget, install it, see what it asks to access and grant it before it runs. The Updates tab updates everything in one click, and removal is a click in the same place. No zip to unpack, no folder to copy into.

![The DeskDash marketplace window on the Widgets tab, a grid of widget cards with a preview and a one-line description each, and an Add button on the ones not yet installed: System Vitals, System Rings, Slot Machine, Power, Now Playing and more, with a search box above and Request a widget and Submit a widget buttons below.](/blog/deskdash-marketplace-browse.png)

The store is open. Anyone can build a widget or a taskbar and [publish it](https://www.desktop-dashboard.com/docs/guides/publishing); submissions run through automatic validation and a review by hand before they appear. Other authors already have work live: [Pulseglass Dock](https://www.desktop-dashboard.com/marketplace/kaladindmp.pulseglass-dock) by kaladindmp is a community taskbar in the [taskbar store](https://www.desktop-dashboard.com/marketplace/taskbars) beside the first-party bars.

![The DeskDash marketplace on the Taskbars tab: Pulseglass Dock, a community taskbar marked In use, beside Rail, Dock and Aurora, each card with a preview, a one-line description and a Use button, with Request a widget and Submit a widget buttons below.](/blog/deskdash-taskbar-store.png)

**Seelen UI**'s built-in widgets are a [clock, media player, system monitor, notifications centre, quick settings panel, flyout alerts, power menu, network manager, Bluetooth manager and clipboard history](https://seelen.io/apps/seelen-ui/desktop-widgets). Beyond those, the resources site carries community resources in six types (themes, widgets, wallpapers, icon packs, plugins and sound packs), and you click a resource on the website to [install it from the app](https://seelen.io/resources). Some listed widgets, such as Better Clipboard, are [marked exclusive for subscribers](https://seelen.io/resources?type=widget). Authors publish by bundling a resource into one YAML file with the CLI and [uploading it through the website](https://seelen.io/docs/resource-guidelines).

## What can a widget reach on your PC, and who decides? [#what-can-a-widget-reach-on-your-pc-and-who-decides]

A **DeskDash** widget runs in an [isolated frame](https://www.desktop-dashboard.com/docs/concepts/security) and gets each capability only after [you grant it](https://www.desktop-dashboard.com/docs/reference/permissions); a **Seelen UI** widget calls the app's commands directly, and its docs describe no grant step.

Each **DeskDash** widget runs in its own frame with no direct reach into files, the network or other widgets. Every system capability goes through the **DeskDash** SDK and is granted per widget: you see what a widget asks for before it runs and decide. A misbehaving widget only affects itself. API keys live in Windows Credential Manager, never in the widget. Taskbars ask the same way. The [security docs](https://www.desktop-dashboard.com/docs/concepts/security) walk through the sandbox layer by layer, and the [permission reference](https://www.desktop-dashboard.com/docs/reference/permissions) lists every permission a widget can request. In the site's own words, security will always be a main concern, and review is a standard the founder keeps working at.

![The DeskDash install prompt for the Weather widget: it asks for internet access, limited to api.open-meteo.com and geocoding-api.open-meteo.com, and for storage inside the dashboard, with an option to exclude permissions before installing.](/blog/deskdash-permission-prompt.png)

Drop to launch shows the model in practice: the app resolves the dropped file, folder or program itself, and no file path ever reaches the widget.

A **Seelen UI** widget is a small web app in HTML, CSS and JavaScript that runs inside a **Seelen UI** [window](https://seelen.io/docs/widget-guidelines). Its JavaScript API gives it lifecycle control, backend calls through `invoke(SeelenCommand.*)` and event subscription; the docs describe `invoke` as a thin, strongly typed wrapper around Tauri's own [invoke](https://seelen.io/docs/widget-js-api), and point at the source files for the full command set. Neither the widget API page nor the widget guidelines describe a permission model, a user grant or a restriction on files, network or processes for widgets; as documented, a widget calls app commands directly.

Toolbar plugins are the one documented sandboxed case. Their scripts run inside a JavaScript sandbox with three whitelisted operations: `invoke` against an [allow-list of commands](https://seelen.io/docs/toolbar-plugins), `open(path)` and `trigger(widgetId)`.

At package level, the [Microsoft Store listing](https://apps.microsoft.com/detail/9p67c2d4t9fb) for **Seelen UI** declares runFullTrust among its capabilities. Microsoft's reference lists runFullTrust as a restricted capability that lets a packaged app run at the [full trust permission level](https://learn.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations) of a classic desktop program.

[DeskDash vs Rainmeter](https://www.desktop-dashboard.com/blog/deskdash-vs-rainmeter) puts the same question to Rainmeter.

## How you configure and theme each one [#how-you-configure-and-theme-each-one]

**DeskDash** is set up in an edit mode on the desktop, with per-widget settings and a theme that follows your wallpaper; **Seelen UI** is set up in its own settings app and themed with CSS or SCSS files.

In **DeskDash**'s edit mode you drag, resize, stack and overlap widgets on a grid, with per-widget settings and a live preview. The theme derives itself from your wallpaper, and a theme builder and a shared design-token system make every widget follow the same look. The Style tab on any widget changes that one widget's background, corners, text colors or fonts while the rest keep following the theme. Layout presets save and switch whole arrangements.

![The Style tab in the Shelf widget's settings: background, border, corner radius, text, muted text and accent colors, a display font and a body font, under a note that the changes apply to this widget only, with a link to edit the theme for every widget.](/blog/deskdash-widget-style-tab.png)

**Seelen UI** toggles its components on and off in its settings app. A theme is a folder with a `metadata.yml` at its root plus CSS or SCSS. Each entry in the styles map is injected only into that widget's webview, and a `sharedStyles` block goes into [every widget](https://seelen.io/docs/theme-guidelines). Several themes can be [applied at once](https://seelen.io/apps/seelen-ui/customizable-shell), later ones overriding earlier ones. The [Seelen docs](https://seelen.io/docs) are written for resource authors, with no user-facing settings reference in the index, and help runs through [Discord first](https://seelen.io/help).

On the authoring side, a **DeskDash** widget is plain HTML, CSS and JavaScript with no build step. Developer mode ships a scaffolder, typed SDK autocomplete and a tray-menu reload, and the docs start with [what a widget is](https://www.desktop-dashboard.com/docs/getting-started/what-is-a-widget). A **Seelen UI** widget is a built web project whose output files are referenced from `metadata.yml`, with no build-free path [documented](https://seelen.io/docs/widget-guidelines).

## Taskbar and window management [#taskbar-and-window-management]

Both replace the Windows taskbar: **DeskDash** with a taskbar you pick from its marketplace, **Seelen UI** with its own dock, SeelenWeg. **Seelen UI** also manages windows, with a tiling window manager, a custom Alt+Tab and virtual desktops, which **DeskDash** leaves to Windows.

A **DeskDash** taskbar takes the place of the Windows taskbar with your pinned apps, open windows, start menu and notification area in its own style. Dock, for example, is a floating macOS-style dock with a magnification wave and live window previews, and Pulseglass Dock is a community entry; the [taskbar marketplace](https://www.desktop-dashboard.com/marketplace/taskbars) has the rest. Right-click a bar for auto-hide, an overlay mode, a Size slider from 50% to 200% and the display that carries it. One taskbar runs at a time, and the Windows taskbar comes back with one click on the "In use" chip. A taskbar is a widget like any other, with the same sandbox and grants, covered in the [taskbar guide](https://www.desktop-dashboard.com/docs/guides/taskbars).

![The DeskDash Dock taskbar at the bottom of a Windows desktop: a floating pill with a Start button and the pinned and running apps as large icons, Brave, File Explorer, Steam, Discord, Terminal and VS Code, each with a small dot under the ones that are open, and a separate tray button beside it.](/blog/deskdash-dock-taskbar.png)

SeelenWeg replaces the Windows taskbar as a full-width bar or as a [macOS-style floating dock](https://seelen.io/apps/seelen-ui/customizable-shell), sits on any edge with auto-hide and window previews, and supports icon size, zoom on hover and drag-and-drop reordering. The Fancy Toolbar is a scriptable status bar with items for CPU, RAM, network, battery, calendar and quick settings. Version 2.8.3 added import of native taskbar pins, and 2.8.0 introduced a [plugin system for the dock](https://github.com/eythaann/Seelen-UI/releases).

Beyond the taskbar, **Seelen UI** has a tiling window manager with four layout engines, a custom Alt+Tab, a [start menu replacement](https://seelen.io/apps/seelen-ui) with app, file and web search, and custom virtual desktops.

## Multi-monitor [#multi-monitor]

Both handle several displays: **DeskDash** keeps a separate layout for each display, and **Seelen UI** runs a separately configured dock and toolbar on each monitor.

A **DeskDash** layout comes back when its display reconnects, and a display with no widgets renders nothing. Settings can shrink the widgets on one screen to 67%, 50% or 33% for 4K and ultrawide displays, and you choose which display carries the taskbar.

**Seelen UI** chooses the dock position [per monitor](https://seelen.io/apps/seelen-ui/customizable-shell), with three visibility modes, and a widget can declare its instances as single, multiple or per-monitor.

## Setup time and the learning curve [#setup-time-and-the-learning-curve]

**DeskDash** installs from the website, and its first run leads you to the store; **Seelen UI** installs from the Microsoft Store, winget or [GitHub Releases](https://github.com/eythaann/Seelen-UI) and needs the WebView2 runtime and Microsoft Edge.

**DeskDash**'s first run shows a setup guide that offers launch at login and hiding the desktop icons. After that it is the store: install, grant, done.

On **Seelen UI**, XDA's [hands-on](https://www.xda-developers.com/seelen-ui-hands-on/) by João Carrasqueira praises the rearrangeable toolbar and the movable dock and criticises poor user education; on discoverability the writer says, "It's all kind of haphazardly thrown in there". One [AlternativeTo reviewer](https://alternativeto.net/software/seelen-ui/about/), kazu, wrote that "there isn't much public information that doesn't require me to join a Discord server".

## Upkeep, updates and stability [#upkeep-updates-and-stability]

**DeskDash** updates itself and the widgets and taskbars you installed, with the release notes shown in the app; **Seelen UI** shipped about one stable release a week from May to September 2026.

**DeskDash** shows the release notes in the app after each update, and the [changelog](https://www.desktop-dashboard.com/changelog) is on the site. Store items update in one click from the Updates tab or on their own. Hidden widgets fully unload, an empty display costs nothing, and a low performance mode exists for weaker machines.

**Seelen UI**'s current release is [v2.8.6, published 18 September 2026](https://github.com/eythaann/Seelen-UI/releases). Across the nineteen weeks from 4 May to 18 September 2026 it put out about one stable release a week, with features in them: transitions for toolbar and dock items, an HDR toggle, night light and the taskbar pin import.

Seelen also publishes a [known-issues page](https://seelen.io/blog/known-issues), last revised 15 July 2026, which lists a launch failure when both the Store and .exe builds are installed and conflicts with window-style modifiers. Taskbar modification apps such as Start11 and StartAllBack may cause problems. On the tracker, [issue #1674](https://github.com/eythaann/Seelen-UI/issues/1674), Win + D hiding the dock and desktop widgets on Windows 11 24H2, has been open since 1 July 2026. On performance, Seelen's FAQ describes the app as lightweight, with usage that varies by configuration, and suggests disabling features for gaming; [XDA](https://www.xda-developers.com/4-reasons-seelen-ui-makes-windows-feel-like-new-operating-system/) says it takes a bit of a toll.

## Price and licence [#price-and-licence]

**DeskDash** is a one-time purchase with every widget and taskbar included; **Seelen UI** is free and open source, with a paid Seelen Sky subscription for extras.

**DeskDash** has no subscription, no ads and no in-app purchases. The purchase buys the app, and every widget and taskbar in the marketplace, community ones included, is free. The current amount is on the [homepage](https://www.desktop-dashboard.com). It runs on Windows 10 and Windows 11, 64 bit.

**Seelen UI** is free under [AGPL-3.0](https://github.com/eythaann/Seelen-UI), installable at no cost from the Store, winget or GitHub. [Seelen Sky](https://seelen.io/sky) sells monthly and annual plans. Each plan lists cloud save and sync across devices, exclusive widgets including Better Clipboard, exclusive themes updated every month and priority support. The subscription gates real catalogue items, since Better Clipboard and Quick Notes are subscriber-only.

## Which one fits you? [#which-one-fits-you]

Pick **DeskDash** for widgets that act and a taskbar from one store, with [each widget's access granted up front](https://www.desktop-dashboard.com/docs/reference/permissions); pick **Seelen UI** for a tiling window manager, virtual desktops and an AGPL licence.

* **You want widgets that act and a taskbar, and nothing else changed.** An app drawer, a shelf you drop files on, tasks, habits, music controls and a taskbar of your choice, all from one store under per-widget grants: that is **DeskDash**.
* **You want to know before install what a widget may reach.** **DeskDash** shows [the grant](https://www.desktop-dashboard.com/docs/reference/permissions) before the widget runs. For **Seelen UI**, the widget API docs describe direct command calls and no grant step.
* **You want to build your own widget or publish one for others.** **DeskDash** widgets are plain HTML, CSS and JavaScript with no build step, and the store takes submissions from anyone. **Seelen UI** authoring is a built web project bundled to YAML.
* **You want a tiling window manager, virtual desktops, a custom Alt+Tab or a start menu with file and web search.** **Seelen UI** ships those and **DeskDash** does not.
* **You want an open source app under AGPL.** **Seelen UI** is AGPL-3.0 and installs from the Store, winget or GitHub.

## Frequently asked

### Does DeskDash have a tiling window manager?

No. **DeskDash** adds widgets and an optional taskbar and leaves window management to Windows. **Seelen UI** has a tiling window manager with four layout engines.

### Does DeskDash need cloud sync like Seelen Sky?

No. **DeskDash** has no subscription; layouts are saved per display and as presets you switch between, and everything is included in the one purchase. Seelen Sky sells cloud save and sync across devices.

### Can you run DeskDash and Seelen UI at the same time?

Both take the place of the Windows taskbar, so only one of them can hold it. **DeskDash** runs one taskbar at a time and hands the Windows taskbar back with one click. Seelen's known-issues page lists conflicts with taskbar modification apps such as Start11 and StartAllBack.

### Does either one replace the Start menu?

A **DeskDash** taskbar carries a start menu in its own style, and the Windows Start menu returns when you switch the Windows taskbar back. **Seelen UI** includes a start menu replacement with app, file and web search.

### Is Seelen UI free?

Yes, the app is free and open source under AGPL-3.0. A [Seelen Sky](https://seelen.io/sky) subscription adds cloud save and sync across devices, exclusive widgets and exclusive themes.

# Desktop gadgets for Windows 11: what replaced the sidebar (/blog/desktop-gadgets-windows-11)

> Windows 11 has no desktop gadgets. Why Microsoft removed them, why the Widgets board is not the same thing, and the apps that put gadgets on the desktop.

## In short

- Windows 11 has no desktop gadgets. Microsoft removed the platform in Windows 8 after a 2012 security advisory, because a gadget ran with every permission of your user account.
- DeskDash puts the classic set on the desktop behind your windows, clock, calendar, system meters, weather and sticky note, plus widgets that do things: an app drawer, task lists, playback and system controls, a game launcher. Each is sandboxed and granted its access before it runs.
- The Windows 11 Widgets board is a flyout over the desktop, and Microsoft's documentation calls it the only place a widget can be shown. Desktop pinning is not supported.
- GadgetPack and Gadgets Revived reinstall the Windows 7 platform, full trust included. Rainmeter draws skins on the desktop. Better Widgets, Widget Launcher and BeWidgets float widgets as windows.

Windows 11 has no desktop gadgets. Microsoft [removed the gadget platform](https://learn.microsoft.com/en-us/windows/compatibility/desktop-gadgets-removed) from Windows in Windows 8 and never brought it back. The built-in **Widgets board** is a [flyout that overlays the desktop](https://learn.microsoft.com/en-us/windows/apps/design/widgets/) from the taskbar or Win+W, and then closes again. **DeskDash** and the other apps below put gadgets back on the desktop itself.

## What desktop gadgets were, and why Microsoft removed them [#what-desktop-gadgets-were-and-why-microsoft-removed-them]

Gadgets arrived with the Windows Vista Sidebar, "a translucent, lockable panel on the desktop" in Microsoft's words, and a gadget could be [dragged off the panel](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/gadgetplatform/introduction-to-the-gadget-platform) onto the desktop. Windows 7 dropped the Sidebar as a container: gadgets were "hosted and displayed on the desktop", though sidebar.exe still owned them all. The set most people remember, going by [Wikipedia's list of the Vista set](https://en.wikipedia.org/wiki/Windows_Desktop_Gadgets), is the clock, calendar, CPU meter, weather, notes, currency, feed headlines and slide show.

The fact that matters most today is how much a gadget was allowed to do. Microsoft's security page puts it under the heading [Full Trust](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/-sidebar-gadget-security): "After the user accepts the warning, the gadget will run with all of the permissions associated with the user's login account". There was no per-gadget grant, and no signing requirement, because certificates were "costly and not commonly used".

That is what ended the platform. On 10 July 2012 Microsoft published [Security Advisory 2719662](https://learn.microsoft.com/en-us/security-updates/securityadvisories/2013/2719662), "Vulnerabilities in Gadgets Could Allow Remote Code Execution", with a Fix it that disabled Windows Sidebar and Gadgets on Vista and Windows 7. It named two routes in. One was a legitimate gadget with a vulnerability: an attacker who exploited it "could take complete control of the affected system" if the user was logged on as an administrator. The other was a malicious gadget the user is tricked into installing. Windows 8 removed gadgets entirely, because "even benign Gadgets could be exploited".

## Is the Windows 11 Widgets board a replacement? [#is-the-windows-11-widgets-board-a-replacement]

No. Microsoft's current widgets documentation, dated July 2026, defines the board as "a flyout plane that overlays the Windows desktop" when you click the Widgets icon on the taskbar, press Windows+W, or [swipe from the left edge](https://learn.microsoft.com/en-us/windows/apps/design/widgets/). The same page states that "the only Widgets host is the Widgets Board built into Windows 11", so a widget cannot be displayed anywhere else.

Every action on Microsoft's [widgets support page](https://support.microsoft.com/en-us/windows/stay-up-to-date-with-widgets-in-windows-7ba79aaa-dac6-4687-b460-ad16a06be6e4) happens inside that board: add with the "+" button, remove from the widget's menu, drag within the pinned section. It never describes placing a widget on the desktop. A [Microsoft Q\&A answer](https://learn.microsoft.com/en-us/answers/questions/5609619/how-to-customize-windows-11-widgets-like-size-posi) from November 2025 lists moving widgets outside the board among the things Windows cannot do.

A Feedback Hub request for desktop pinning reached 214 upvotes, and a Microsoft engineer replied that ["this isn't something we currently support"](https://www.xda-developers.com/windows-11-lock-screen-widgets-get-boost-desktop-pinning-remains-ignored/). Pocket-lint's verdict in December 2025: it is "still frustratingly impossible to [place them onto your desktop](https://www.pocket-lint.com/better-widgets-app-windows-11-light-desktop-widgets/) in the vein of Windows Vista and 7 Gadgets".

What Microsoft is changing is the board's noise, not its place. In Insider Experimental Build 26300.8346 from 1 May 2026, it is testing [quiet-by-default settings](https://blogs.windows.com/windows-insider/2026/05/01/announcing-new-builds-for-1-may-2026-and-extending-iso-support/): open-on-hover off, taskbar badging off, and taskbar alerts limited until you choose to open Widgets. The board costs nothing beyond Windows itself.

## What puts gadgets on a Windows 11 desktop today? [#what-puts-gadgets-on-a-windows-11-desktop-today]

Seven apps do it, on the desktop or in floating windows. The **Widgets board** is the last row, as the baseline.

| Tool                     | Where widgets live                    | What a widget can do on the PC                                                                                                                                         | Where widgets come from                                                                                           | Maintained                   | Price                               |
| ------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------------------------------- |
| Rainmeter                | On the desktop                        | A skin can [run any program](https://docs.rainmeter.net/manual/plugins/runcommand/), hidden, no prompt                                                                 | Skin sites: DeviantArt, forums, Reddit, GitHub                                                                    | 4.5.26, 20 May 2026          | Free, GPL v2                        |
| DeskDash                 | On the desktop, behind normal windows | Only [what you grant](https://www.desktop-dashboard.com/docs/reference/permissions), per widget, [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) | Built-in [marketplace](https://www.desktop-dashboard.com/marketplace), first-party and community, also on the web | Current app, updates itself  | One-time purchase, all widgets free |
| GadgetPack               | On the desktop                        | [Everything your account can](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/-sidebar-gadget-security), Windows 7 platform                | 50+ bundled, plus .gadget files                                                                                   | 42.0, 11 May 2026            | Free                                |
| Gadgets Revived          | On the desktop and sidebar            | [Everything your account can](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/-sidebar-gadget-security), Windows 7 platform                | Its own catalogue                                                                                                 | No version or date published | Not stated                          |
| Better Widgets           | Floating, resizable, lockable         | Not stated                                                                                                                                                             | Ten vendor widget types                                                                                           | Microsoft Store listing      | Paid, one-time purchase             |
| Widget Launcher          | Floating windows                      | Third-party extensions in a [full-trust Store app](https://apps.microsoft.com/store/detail/9wzdncrdqfbt?hl=en-us\&gl=us)                                               | Microsoft Store                                                                                                   | Microsoft Store listing      | Free download                       |
| BeWidgets                | Floating windows                      | The vendor's own widgets, [full-trust Store app](https://apps.microsoft.com/detail/9nq07fg50h2q)                                                                       | The vendor's own                                                                                                  | 1.0.28.0, 12 March 2026      | Paid premium option                 |
| Windows 11 Widgets board | In a flyout over the desktop          | [Adaptive Cards](https://learn.microsoft.com/en-us/windows/apps/design/widgets/) fed by a packaged app or PWA                                                          | Apps that ship a widget provider                                                                                  | Docs dated July 2026         | Included with Windows               |

### DeskDash [#deskdash]

**DeskDash** puts widgets on the desktop, behind your normal windows. They are click-through except where a widget is interactive, so the desktop stays a desktop: nothing to open, nothing to close, and basically invisible to your resource usage.

![A DeskDash desktop on a pale misty forest wallpaper: the day name in wide letters across the sky, a clock panel with the date, a row of power controls, a Games drawer of installed Steam games with their cover art, a month calendar, a Now Playing card with transport controls and a task list, all sitting on the wallpaper.](/showcase/deskdash-desktop-misty-forest.webp)

The classic set is in the marketplace. A [Clock](https://www.desktop-dashboard.com/marketplace/deskdash.clock) with big readable time and date. A [Calendar](https://www.desktop-dashboard.com/marketplace/deskdash.calendar) with month and week views, where you click a day to add an event. [System Vitals](https://www.desktop-dashboard.com/marketplace/deskdash.system-vitals) with live CPU, RAM, GPU, disk and network sparklines, or [System Rings](https://www.desktop-dashboard.com/marketplace/deskdash.system-rings) with the same numbers as radial gauges. [Weather](https://www.desktop-dashboard.com/marketplace/deskdash.weather) for your city through Open-Meteo, with no API key to set up. A [Sticky Note](https://www.desktop-dashboard.com/marketplace/deskdash.stickynotes) that keeps whatever you type on it. But also new ones are added every single day, and docs are available if you want to build your own. Each installs with one click from the built-in [marketplace](https://www.desktop-dashboard.com/marketplace), and every listing is browsable on the website without the app.

What sets them apart from a gadget is what they do. The widgets are interactive and keep their state. [Desktop Drawer](https://www.desktop-dashboard.com/marketplace/deskdash.desktop-drawer) turns the desktop or any folder into an app drawer that collapses to a slim handle on a screen edge. [Todo](https://www.desktop-dashboard.com/marketplace/deskdash.todo) and [Habits](https://www.desktop-dashboard.com/marketplace/deskdash.habits) keep the lists you tick off, and [Schedule](https://www.desktop-dashboard.com/marketplace/deskdash.schedule) carries a weekly timetable with reminders. [Now Playing](https://www.desktop-dashboard.com/marketplace/deskdash.now-playing) controls the music, [System Controls](https://www.desktop-dashboard.com/marketplace/deskdash.system-controls) switches wifi, volume and battery from one bar, and [Steam Recent](https://www.desktop-dashboard.com/marketplace/deskdash.steam-recent) lists your last Steam games with hours and achievements and launches one on click. Widgets and taskbars can also list the Steam and GOG games installed on the PC and start them, with nothing to sign in to.

Each widget runs sandboxed. Before you install it you see exactly what it can access and decide what to grant, per widget. A widget has no direct reach into files, the network or other widgets, and a misbehaving widget only affects itself. That is the direct answer to the old platform's Full Trust, where one accepted warning handed a gadget every permission of your account. The [security docs](https://www.desktop-dashboard.com/docs/concepts/security) describe the sandbox layer by layer.

![The DeskDash install prompt for the Weather widget: it asks for internet access, limited to api.open-meteo.com and geocoding-api.open-meteo.com, and for storage inside the dashboard, with an option to exclude permissions before installing.](/blog/deskdash-permission-prompt.png)

Every display gets its own layout, and layouts come back when a display reconnects. The same app can replace the Windows taskbar with one from its [taskbar marketplace](https://www.desktop-dashboard.com/marketplace/taskbars), a macOS-style dock among them, under the same sandbox and grants, and the Windows taskbar comes back with one click.

Widgets are plain HTML, CSS and JavaScript, and anyone can build and submit one; other authors already have, and [Pulseglass Dock](https://www.desktop-dashboard.com/marketplace/kaladindmp.pulseglass-dock) is a community taskbar. **DeskDash** runs on Windows 10 and Windows 11, 64 bit. It is a one-time purchase with no subscription, every widget and taskbar is free, and the current amount is on the [download page](https://www.desktop-dashboard.com/download). Against the skinning tools, [Rainmeter alternatives in 2026](https://www.desktop-dashboard.com/blog/rainmeter-alternatives) covers what each one's widgets can reach on your PC.

### GadgetPack, with Gadgets Revived [#gadgetpack-with-gadgets-revived]

[**GadgetPack**](https://gadgetpack.net/) is at version 42.0, released 11 May 2026, and it installs "the Gadget Platform that was originally present in Windows 7 with some modifications that make it work on newer Operating Systems". It ships more than 50 gadgets, among them CPU, GPU, network and drive meters, a calendar, clocks, weather and sticky notes, and it accepts custom .gadget files. It is free, with an optional Donate link and no paid tier.

Because the platform it revives is the Windows 7 one, a gadget runs with all the permissions of your user account, and Microsoft states there were "no significant changes to the security model" between Vista and Windows 7. **GadgetPack**'s [own FAQ](https://gadgetpack.net/faq.html) says it plainly: "Gadgets can contain viruses like any other software." Its advice is to trust the source and use antivirus.

Three practical notes. On Windows 11 the gadget entries sit behind "Show more options" in the right-click menu. AVG's Identity Protection can block **GadgetPack** silently; the fix is antivirus exceptions for 8GadgetPack.exe and sidebar.exe. And a Windows update can uninstall it: the [same FAQ](https://gadgetpack.net/faq.html) says to open the "Restore gadgets" shortcut it leaves on the desktop, or to reinstall the current version.

[**Gadgets Revived**](https://gadgetsrevived.com/) is the second revival installer: "Classic Desktop gadgets and sidebar from Windows 7 working in Windows 11 and Windows 10", with Windows 8.1 also supported, and a catalogue from Battery to Utility that includes Clock, Calendar, Weather and System Information. The [download](https://gadgetsrevived.com/download-sidebar/) is a zip: unpack, run, pick a language, install. The page shows no version number, release date or security statement.

### Rainmeter [#rainmeter]

[**Rainmeter**](https://www.rainmeter.net/) is free and open source under the GNU GPL v2, with its source public [on GitHub](https://github.com/rainmeter/rainmeter). The current release is [4.5.26](https://github.com/rainmeter/rainmeter/releases), published 20 May 2026. A skin is a plain-text INI file: measures gather values, meters display them. Customizing means editing that file; a settings panel exists only where the skin author built one.

Skins come from DeviantArt, Reddit, the forum and GitHub, and **Rainmeter**'s own [manual](https://docs.rainmeter.net/manual/getting-started/customizing/) warns that "most sites have no policy of verifying either the identity of the uploader or the integrity of the files". The [RunCommand plugin](https://docs.rainmeter.net/manual/plugins/runcommand/) lets a skin execute any external program, hidden by default, with no prompt to the user. One thing no other tool here does: with HWiNFO installed and running, **Rainmeter** reads its [sensor values through the Windows Registry](https://docs.rainmeter.net/tips/hwinfo/). The head-to-head is in [DeskDash vs Rainmeter](https://www.desktop-dashboard.com/blog/deskdash-vs-rainmeter).

### Widget apps from the Microsoft Store [#widget-apps-from-the-microsoft-store]

Three Store apps put widgets in floating windows.

[**Better Widgets**](https://www.pocket-lint.com/better-widgets-app-windows-11-light-desktop-widgets/) offers ten widget types, among them calendar, clock, system monitors, notes, to-do lists and weather. Once pinned to the desktop, widgets resize like windows, with an option to lock them in place. Pocket-lint lists it as a paid app on the Microsoft Store, a one-time purchase with no ads and no in-app purchases.

[**Widget Launcher**](https://apps.microsoft.com/store/detail/9wzdncrdqfbt?hl=en-us\&gl=us) is a free download on the Store. Its third-party extensions run inside a full-trust Store app with no documented permission model, and Pocket-lint notes that it [shows brief ads](https://www.pocket-lint.com/apps-to-bring-widgets-to-windows-11-desktop/) when loading new widgets.

[**BeWidgets**](https://www.softpedia.com/get/System/OS-Enhancements/BeWidgets.shtml) is at version 1.0.28.0, updated 12 March 2026, for Windows 10 and 11, with widgets for time, date, photos, finance and app shortcuts. It runs the vendor's own widgets only, inside a full-trust Store app.

## How to choose [#how-to-choose]

Four questions settle it.

* **On the desktop or in a panel?** **DeskDash**, **GadgetPack**, **Gadgets Revived** and **Rainmeter** put widgets on the desktop; the Store apps float them as windows, and the **Widgets board** is a flyout you open and close.
* **Which gadget do you need?** **DeskDash**'s marketplace carries the clock, todo, pomodoro timer, sticky note, system meters and a Steam games launcher, among many others. **GadgetPack** has meters, weather and sticky notes, **Gadgets Revived** has System Information, Weather and Clock categories, and **Better Widgets** has system monitors, notes and weather.
* **How does it get updated?** **DeskDash** updates itself, and its widgets update from the marketplace. A Windows update can uninstall **GadgetPack**, and its FAQ documents the restore step. **Gadgets Revived** publishes no version number or release date. **Rainmeter**'s current release is from May 2026.
* **What can it do without asking?** **DeskDash** shows you [what each widget can access](https://www.desktop-dashboard.com/docs/reference/permissions) and asks before it runs. The Windows 7 platform runs a gadget with your account's full permissions after one warning. A **Rainmeter** skin can run a hidden program with no prompt.

## Frequently asked

### Does Windows 11 have desktop gadgets?

No. Microsoft removed gadgets in Windows 8 and never brought them back. The **Windows 11 Widgets board** is a flyout over the desktop, and Microsoft's documentation states it is the only place a widget can be shown.

### Can you get the Windows 7 gadgets on Windows 11?

Yes. **GadgetPack** and **Gadgets Revived** both reinstall the Windows 7 gadget platform on Windows 11. With **GadgetPack**, the gadget entries sit behind "Show more options" in the right-click menu. More modern systems should look toward apps like **DeskDash** and **Rainmeter**.

### Are desktop gadgets safe?

A gadget on the Windows 7 platform runs with all the permissions of your login account once you accept its warning. Microsoft's 2012 advisory named two routes of attack, a legitimate gadget with a vulnerability or a malicious one you are tricked into installing, and warned that gadgets can access your files and change their behavior at any time. **GadgetPack**'s own FAQ says gadgets can contain viruses like any other software, so trust the source and run antivirus.

### What is the difference between gadgets and widgets?

Gadgets are mini-applications built from XML, HTML, CSS and script, hosted on the desktop by the sidebar.exe process. A Windows 11 widget is an Adaptive Card fed by a packaged app or a PWA, and it shows only inside the **Widgets board**.

### How do you put widgets on the desktop instead of the Widgets panel?

Windows itself offers no way: the board is the only host, and a Microsoft engineer told Feedback Hub voters that desktop pinning is not supported. A desktop widget app does it: **DeskDash** puts widgets on the desktop from its [marketplace](https://www.desktop-dashboard.com/marketplace), **GadgetPack** and **Rainmeter** do too, and the Store apps float them as windows.

# DeskDash vs Rainmeter: what each one can reach on your PC (/blog/deskdash-vs-rainmeter)

> DeskDash vs Rainmeter, side by side: where widgets come from, what each one can run on your PC, sensors, multi-monitor, taskbar and price.

## In short

- DeskDash and Rainmeter both put live widgets on the desktop. A DeskDash widget is a web module you install in one click from a marketplace inside the app; a Rainmeter skin is an INI file you edit in a text editor and download from DeviantArt or a forum.
- A DeskDash widget is sandboxed and asks for each capability before it runs. A Rainmeter skin can run any program on the PC, can hide its window, and no prompt to you is documented.
- DeskDash reads CPU, RAM, GPU, disk, network, drive space, battery and wifi, and each widget has a setting for which of them it shows. Temperatures, voltages and fan speeds are not in that set today; they are planned. Rainmeter reads those through CoreTemp, SpeedFan or HWiNFO, each running alongside it.
- DeskDash is a one-time purchase and replaces the taskbar with custom ones. Rainmeter is free under GPL v2 and states that it cannot change your taskbar.

**DeskDash** and **Rainmeter** both put live widgets on the wallpaper of a Windows desktop. **DeskDash** is a one-time purchase: [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) web widgets and/or taskbars from an in-app marketplace, each [granted its capabilities](https://www.desktop-dashboard.com/docs/reference/permissions) before it runs. **Rainmeter** is [free under GPL v2](https://www.rainmeter.net/), a skin is a [plain-text INI file](https://docs.rainmeter.net/manual/getting-started/skin-anatomy/) from [DeviantArt or a forum](https://www.rainmeter.net/discover), and a skin can [run any program](https://docs.rainmeter.net/manual/plugins/runcommand/). Which fits depends on what you want from the desktop.

## DeskDash and Rainmeter side by side [#deskdash-and-rainmeter-side-by-side]

|                         | DeskDash                                                                                                                                                                                | Rainmeter                                                                                                                                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| What it is              | [Sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) web widgets (HTML, CSS, JavaScript) on the wallpaper                                                              | [INI skin engine](https://docs.rainmeter.net/manual/getting-started/skin-anatomy/): measures in, meters out                                                                    |
| Where widgets come from | Built-in marketplace, first-party and community, every listing free                                                                                                                     | [DeviantArt, Reddit, forum, GitHub](https://docs.rainmeter.net/manual/getting-started/customizing/); no first-party catalogue                                                  |
| Installing one          | One click in the marketplace                                                                                                                                                            | Download a .rmskin, double-click, [Skin Installer](https://docs.rainmeter.net/manual/installing-skins/)                                                                        |
| Configuring one         | Edit mode: drag, resize, per-widget settings with live preview                                                                                                                          | Right-click, Edit skin, change the .ini in a text editor                                                                                                                       |
| What a widget can reach | [Isolated](https://www.desktop-dashboard.com/docs/concepts/security); each capability [granted per widget](https://www.desktop-dashboard.com/docs/reference/permissions) before it runs | [RunCommand](https://docs.rainmeter.net/manual/plugins/runcommand/) runs any program, can hide its window, no documented prompt                                                |
| Hardware sensors        | System Vitals and System Rings: CPU, RAM, GPU, disk, network                                                                                                                            | [CoreTemp, SpeedFan](https://docs.rainmeter.net/manual/plugins/coretemp/) plugins, [HWiNFO via registry](https://docs.rainmeter.net/tips/hwinfo/); each needs that app running |
| Multi-monitor           | Own layout per display; layouts return when a display reconnects                                                                                                                        | Position per skin with an [@N screen index](https://docs.rainmeter.net/manual/settings/skin-sections/); drag between monitors                                                  |
| Taskbar                 | Replaces the Windows taskbar: custom ones                                                                                                                                               | ["It cannot change the appearance of your taskbar"](https://docs.rainmeter.net/manual/getting-started/)                                                                        |
| Updates                 | App auto-updates; marketplace Updates tab for widgets and taskbars                                                                                                                      | App [updates itself](https://docs.rainmeter.net/manual/settings/rainmeter-section/); no documented way for a skin to update itself                                             |
| Licence and price       | One-time purchase, no subscription, every widget and taskbar included                                                                                                                   | [Free, GPL v2](https://github.com/rainmeter/rainmeter)                                                                                                                         |
| Windows                 | Windows 10 and 11, 64 bit                                                                                                                                                               | [Windows 7 or above](https://www.rainmeter.net/)                                                                                                                               |

## What is each one? [#what-is-each-one]

**DeskDash** is a Windows app that turns the desktop into a live dashboard of [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) web widgets, with a taskbar from the same store; **Rainmeter** is free, open source software whose skins are plain-text INI files.

**DeskDash** widgets stay behind your normal windows and are click-through except where they are interactive. The same app can replace the Windows taskbar, and every widget is a plain web page built on a documented SDK.

**Rainmeter** is open source software, free of charge under the GNU GPL v2. A skin is a plain-text INI file with exactly [three kinds of line](https://docs.rainmeter.net/manual/getting-started/skin-anatomy/), and the manual's minimal skin is four sections that print CPU use as text. Both belong to the wider field of [desktop widget tools for Windows](https://www.desktop-dashboard.com/blog/rainmeter-alternatives).

## Where do the widgets come from? [#where-do-the-widgets-come-from]

**DeskDash** widgets come from one marketplace inside the app; **Rainmeter** skins come from DeviantArt, Reddit, the forums and GitHub, with no first-party catalogue.

The **DeskDash** [built-in marketplace](https://www.desktop-dashboard.com/marketplace) has Widgets and Taskbars sections, each with Installed, Browse and Updates tabs, and you find, install, grant, update and remove everything there. Every listing is free, and the catalogue keeps growing with work by other authors beside the first-party widgets: [Pulseglass Dock](https://www.desktop-dashboard.com/marketplace/kaladindmp.pulseglass-dock) by kaladindmp is a community taskbar. Clocks, system stats and weather are the plain end of it. The rest do things and keep their state: [Desktop Drawer](https://www.desktop-dashboard.com/marketplace/deskdash.desktop-drawer) turns the desktop or any folder into an app drawer that collapses to a handle on a screen edge, [Todo](https://www.desktop-dashboard.com/marketplace/deskdash.todo) and [Habits](https://www.desktop-dashboard.com/marketplace/deskdash.habits) keep the lists you tick off, [Now Playing](https://www.desktop-dashboard.com/marketplace/deskdash.now-playing) controls the music, [System Controls](https://www.desktop-dashboard.com/marketplace/deskdash.system-controls) switches wifi, volume and battery from one bar, and [Steam Recent](https://www.desktop-dashboard.com/marketplace/deskdash.steam-recent) lists your last Steam games and launches one on click. Every listing is browsable on the website without the app, a "Request a widget" form sits in the store footer, and anyone can submit. Submissions are validated and reviewed before they publish.

![The DeskDash marketplace window on the Widgets tab, a grid of widget cards with a preview and a one-line description each, and an Add button on the ones not yet installed: System Vitals, System Rings, Slot Machine, Power, Now Playing and more, with a search box above and Request a widget and Submit a widget buttons below.](/blog/deskdash-marketplace-browse.png)

**Rainmeter**'s own Discover page sends you to four places: DeviantArt, which it calls ["one of the largest online repositories of Rainmeter resources"](https://www.rainmeter.net/discover), the r/Rainmeter subreddit, the **Rainmeter** forums and a Lifehacker desktop pool. The manual's list adds GitHub and Flickr, notes that DeviantArt [needs an account](https://docs.rainmeter.net/manual/getting-started/customizing/), and names no first-party catalogue and no in-app store. XDA describes the result: a web search for skins ["returns a series of unofficial websites"](https://www.xda-developers.com/windhawk-vs-rainmeter/).

The famous suites are in mixed shape. [Enigma](https://github.com/kaelri/enigma) was last pushed on 18 May 2024. The [Monstercat visualizer](https://github.com/marcopixel/monstercat-visualizer), at 962 stars, was last pushed on 17 June 2023. [Droptop Four](https://github.com/Droptop-Four/Droptop-Four) was last pushed on 29 April 2026, shipped [five releases](https://github.com/Droptop-Four/Droptop-Four/releases) that month, and updates by a download from its own site.

Long-time users say the same. One four-star AlternativeTo reviewer writes that ["many old Rainmeter downloads are now forever lost, because sites like Customize.org are long dead"](https://alternativeto.net/software/rainmeter/reviews/) and that the community is looking for "another place to upload and archive their work". A five-star reviewer says "the template/layouts haven't been updated in a while" and "you have to trust sites like Deviant Art".

**Rainmeter**'s manual is candid about those sites: ["most sites have no policy of verifying either the identity of the uploader or the integrity of the files"](https://docs.rainmeter.net/manual/getting-started/customizing/). It tells you never to download an .exe posing as a skin installer and to read the comments for malware reports. The Discover page [repeats the caution](https://www.rainmeter.net/discover) for anything not from rainmeter.net.

## How do you install and configure one? [#how-do-you-install-and-configure-one]

A **DeskDash** widget installs in one click from the marketplace and is configured in a settings pane; a **Rainmeter** skin installs from a .rmskin package and is configured by editing its .ini file.

In **DeskDash**, edit mode drags, resizes, restyles, stacks and overlaps widgets on a grid, and every widget has its own settings with live preview. If you can make a web page, you can [write a widget](https://www.desktop-dashboard.com/docs): HTML, CSS and JavaScript, no build step, with a scaffolder and typed SDK autocomplete in developer mode.

A **Rainmeter** skin arrives as a .rmskin package. You double-click it, the [Skin Installer](https://docs.rainmeter.net/manual/installing-skins/) shows its options, and you click Install; one package can carry skins, layouts and plugins.

To change one, you right-click the skin, choose [Edit skin](https://docs.rainmeter.net/manual/getting-started/customizing/), and the .ini opens in Notepad or your default text editor. You find the Variables section, change values, save and refresh. A graphical settings screen exists only if the skin's author built one, which the manual calls "more the exception than the rule". The manual's own ladder: editing a skin ["may be scary if you are not a programmer"](https://docs.rainmeter.net/manual/getting-started/), and writing one sits at HTML or JavaScript difficulty. XDA calls that config-file editing ["intimidating for beginners"](https://www.xda-developers.com/windhawk-vs-rainmeter/).

## What can a widget do to your PC? [#what-can-a-widget-do-to-your-pc]

A **DeskDash** widget runs [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) and gets each capability only after [you grant it](https://www.desktop-dashboard.com/docs/reference/permissions); a **Rainmeter** skin can run any program through its RunCommand plugin, and no prompt to you is documented.

Each **DeskDash** widget runs in a [sandbox](https://www.desktop-dashboard.com/docs/concepts/security), isolated, with no direct reach into files, the network or other widgets; all system access flows through the SDK. Before a widget runs you see [exactly what it can access](https://www.desktop-dashboard.com/docs/reference/permissions) and grant each capability per widget, and taskbars ask the same way. A misbehaving widget only affects itself; the app and the other widgets keep running. API keys live in Windows Credential Manager, never in a widget, and every marketplace submission is validated and reviewed before it publishes.

![The DeskDash install prompt for the Weather widget: it asks for internet access, limited to api.open-meteo.com and geocoding-api.open-meteo.com, and for storage inside the dashboard, with an option to exclude permissions before installing.](/blog/deskdash-permission-prompt.png)

**Rainmeter**'s RunCommand plugin ["executes an external command or program in Windows"](https://docs.rainmeter.net/manual/plugins/runcommand/) and hands the output back to the skin. It defaults to cmd.exe, the Parameter option is the whole command line, and the State option "attempts to hide the program's window". The RunCommand page carries no security warning and describes no prompt to you before a skin runs a command.

Skins also run [Lua 5.1 scripts](https://docs.rainmeter.net/manual/lua-scripting/) on load, on update or on command. The manual names five unavailable functions; the io and os libraries are not on that list. The older [!Execute bang](https://docs.rainmeter.net/manual/bangs/), which ran external applications, is now "optional. Do not use it in new skins".

Third-party [plugin .dll files](https://docs.rainmeter.net/manual/plugins/) ship inside skin packages and install with the skin. The install docs mention [no signature or integrity check](https://docs.rainmeter.net/manual/installing-skins/) on a .rmskin.

## Does DeskDash read hardware sensors the way Rainmeter does? [#does-deskdash-read-hardware-sensors-the-way-rainmeter-does]

**DeskDash** reads CPU, RAM, GPU, disk, network, drive space, battery and wifi with no second app to run; **Rainmeter** also reaches temperatures, voltages and fan speeds, through CoreTemp, SpeedFan or HWiNFO running beside it.

**DeskDash** samples the machine once a second and hands the reading to every widget you granted system access: CPU, RAM, GPU, disk and network, plus drive space, battery and wifi. Which of them a tile shows is a setting on that tile, so two copies of the same widget can watch different things. Examples are [System Vitals](https://www.desktop-dashboard.com/marketplace/deskdash.system-vitals) or [System Rings](https://www.desktop-dashboard.com/marketplace/deskdash.system-rings). But indeed, stuff like temperatures, voltages and fan speeds are not in that set today, though they are planned for the future.

**Rainmeter** reads those from a second application. The [CoreTemp plugin](https://docs.rainmeter.net/manual/plugins/coretemp/) reports per-core temperature, TjMax, VID, TDP and power draw, and the CoreTemp app "must be running in the background". The [SpeedFan plugin](https://docs.rainmeter.net/manual/plugins/speedfan/) gives temperature, fan speed and voltage on the same terms. HWiNFO is read another way again: version 7.02 or later writes its sensors to the Windows Registry, and a Registry measure picks them up after a [five-step setup](https://docs.rainmeter.net/tips/hwinfo/) and a reg query for each sensor's index.

## How do they handle several monitors, scaling and the taskbar? [#how-do-they-handle-several-monitors-scaling-and-the-taskbar]

**DeskDash** keeps a layout per display and can replace the Windows taskbar; **Rainmeter** positions each skin per screen and states that it cannot change the taskbar.

In **DeskDash** every display gets its own layout. Layouts remember their display and come back when it reconnects, and a display with no widgets renders nothing. For 4K and ultrawide screens, Settings shrinks the widgets on one display to 67%, 50% or 33%, and layout presets save whole arrangements. The same app replaces the Windows taskbar with one from its [taskbar marketplace](https://www.desktop-dashboard.com/marketplace/taskbars), the macOS-style Dock below among them, under the same sandbox and grants, and the Windows taskbar comes back with one click.

![The DeskDash Dock taskbar at the bottom of a Windows desktop: a floating pill with a Start button and the pinned and running apps as large icons, Brave, File Explorer, Steam, Discord, Terminal and VS Code, each with a small dot under the ones that are open, and a separate tray button beside it.](/blog/deskdash-dock-taskbar.png)

In **Rainmeter** a skin's position is set per skin, relative to the primary screen by default, or with an [@N index](https://docs.rainmeter.net/manual/settings/skin-sections/) from 1 to 32 for a given screen, or 0 for the virtual desktop. You [drag skins](https://docs.rainmeter.net/manual/arranging-skins/) with the mouse, positions are remembered across restarts, and with the monitor-boundary setting on a skin jumps whole onto the other monitor. The position and arrangement pages document no DPI or display-scaling handling, so scaling is the skin author's job. On the taskbar **Rainmeter** is explicit: it ["cannot change the appearance of your taskbar, Start button, desktop icons, file explorer, or other built-in Windows components"](https://docs.rainmeter.net/manual/getting-started/), and it is "not a window manager".

## How do updates and upkeep work? [#how-do-updates-and-upkeep-work]

**DeskDash** updates itself and updates widgets and taskbars from the marketplace; **Rainmeter** updates the application, with no documented way for a skin to update itself.

**DeskDash** shows release notes in the app after each update and publishes a [changelog](https://www.desktop-dashboard.com/changelog). The marketplace Updates tab updates every widget and taskbar in one click, or they update on their own.

**Rainmeter**'s current release is [4.5.26, published 20 May 2026](https://github.com/rainmeter/rainmeter/releases). The last three years brought one release in 2023, two in 2024, three in 2025 and three in 2026, in clusters with two gaps of 12 and 13 months. The source repository [received commits](https://github.com/rainmeter/rainmeter/commits/master) on 2 September 2026.

**Rainmeter**'s [global settings](https://docs.rainmeter.net/manual/settings/rainmeter-section/) update the application only. **Rainmeter** documents no way for an installed skin to update itself; Droptop Four ships its own download-based updates instead.

## What does each one cost, and what does it run on? [#what-does-each-one-cost-and-what-does-it-run-on]

**DeskDash** is a one-time purchase with every widget and taskbar included; **Rainmeter** costs nothing.

**DeskDash** has no subscription, the current amount is on the [download page](https://www.desktop-dashboard.com/download), and it runs on Windows 10 and 11, 64 bit.

**Rainmeter** [costs nothing](https://www.rainmeter.net/), with no paid tier and no licence key. It runs on [Windows 7 or above](https://www.rainmeter.net/), and its deep sensor routes need CoreTemp, SpeedFan or HWiNFO installed and running.

## Which one fits you? [#which-one-fits-you]

Pick **DeskDash** for widgets that act, a taskbar from the same store and [a grant before each widget runs](https://www.desktop-dashboard.com/docs/reference/permissions); pick **Rainmeter** for deep sensor readouts through HWiNFO and a skin you write line by line.

You want widgets you install in a click, configure in a settings pane, and update from one place, with a record of what each one may touch before it runs. That is **DeskDash**'s marketplace, [permission model](https://www.desktop-dashboard.com/docs/reference/permissions) and [sandbox](https://www.desktop-dashboard.com/docs/concepts/security).

You want widgets that do things rather than show things: an app drawer, a task list, playback and system controls, a game launcher, each keeping its state between sessions. That is **DeskDash**'s interactive widgets with storage.

You want a dock or a different taskbar from the same app that runs your widgets, and a layout per display across two or more screens. That is **DeskDash**'s taskbar marketplace and per-display layouts.

You build widgets and already know HTML, CSS and JavaScript. **DeskDash**'s SDK and docs are written for you. **Rainmeter**'s authoring language is its INI format plus Lua.

You want per-core temperatures, voltages or fan speeds on the wallpaper, and you already run HWiNFO or CoreTemp. That reach is **Rainmeter**'s, through the plugin or registry routes.

You want a text-editor workflow and a skin you wrote line by line. A **Rainmeter** skin is an INI file, and the manual puts writing one at HTML or JavaScript difficulty.

## Frequently asked

### Does DeskDash read hardware sensors like HWiNFO?

**DeskDash** reads CPU, RAM, GPU, disk, network, drive space, battery and wifi, and each widget has a setting for which of them it shows. System Vitals draws them as sparklines and System Rings as radial gauges, with no second app to run. Temperatures, voltages and fan speeds are not in that set today; they are planned. **Rainmeter** reads those through CoreTemp, SpeedFan or HWiNFO, each running alongside it.

### Can Rainmeter skins be converted to DeskDash widgets?

A skin is an INI file of measures and meters; a **DeskDash** widget is a web page in HTML, CSS and JavaScript. Moving one across means writing the widget, starting with the [first-widget tutorial](https://www.desktop-dashboard.com/docs/getting-started/your-first-widget).

### Is Rainmeter still maintained?

Yes. Version 4.5.26 shipped on 20 May 2026, the repository received commits on 2 September 2026, and releases arrive in clusters separated by year-long gaps.

### Is Rainmeter safe?

The application is free and open source. What a skin can do is the real question: RunCommand runs any program, can hide its window, and no prompt to the user is documented. **Rainmeter**'s own manual says most skin sites verify neither uploader nor files.

### Does Rainmeter work on Windows 11?

The stated requirement is Windows 7 or above. On 24H2, Show Desktop hid skins from June 2024 until the fix merged in April 2025.

# Rainmeter alternatives in 2026: which ones are still maintained, and what each widget can reach on your PC (/blog/rainmeter-alternatives)

> Rainmeter alternatives audited for August 2026: last release, licence, real price, and what a widget can reach on your PC once installed.

## In short

- Still maintained in 2026: DeskDash, Seelen UI, Themia, GadgetPack, Widget Launcher and BeWidgets. XWidget is discontinued, and its download server is reported down.
- DeskDash widgets do work, not just readouts: a folder becomes an app drawer, the todo list keeps its ticks, Now Playing runs the music, and Steam Recent launches your last game.
- DeskDash is the only tool in this set that asks you to grant each capability before a widget runs. A Rainmeter skin can run any program with your full user rights, with a hidden window and no prompt.
- Rainmeter is alive and free (4.5.26 shipped in May 2026). People switch over what a skin can reach and over old skin sites closing, not over abandonment.
- Wallpaper Engine and Lively do wallpapers, not widgets. Deep sensor work still belongs to Rainmeter with HWiNFO.

The maintained Rainmeter alternatives in 2026 are **DeskDash** ([a one-time purchase](https://www.desktop-dashboard.com), interactive widgets and taskbars on the wallpaper, every one [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) and free), **Seelen UI** (free and open source, a whole desktop suite: widgets, a dock and a tiling window manager), **Themia** (freemium, the vendor's own 22 widgets), **GadgetPack** (free, the Windows 7 gadgets) and **Widget Launcher** (free, Microsoft Store). **Wallpaper Engine** and **Lively** do animated wallpaper only, no widgets. **Rainmeter** itself is not dead: it is free, open source and shipped [version 4.5.26](https://github.com/rainmeter/rainmeter/releases) on 20 May 2026. People look for alternatives because a skin can run any program with your full user rights, and because old skin sites are closing.

Below, every tool by last release date, licence, real price, where widgets come from, and what a widget can reach on your machine once it runs.

## The audit table [#the-audit-table]

Prices were read on vendor pages on 26 August 2026. Steam figures move with live discounts.

| Tool                     | Licence     | Price                                        | Widgets come from                                         | What a widget can reach                                                                                                                                                                                                 | Verdict                                                  |
| ------------------------ | ----------- | -------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| DeskDash                 | Proprietary | One-time purchase                            | Built-in marketplace, first-party and community, all free | [Sandboxed](https://www.desktop-dashboard.com/docs/concepts/security), each capability [granted per widget](https://www.desktop-dashboard.com/docs/reference/permissions) before it runs, you can deny some permissions | Alive; interactive widgets and taskbars on the wallpaper |
| Rainmeter                | GPL-2.0     | Free                                         | Third-party skin sites, unverified                        | A skin can [run any command](https://github.com/rainmeter/rainmeter-docs/blob/master/source/manual/plugins/runcommand.html) with your user rights, no prompt                                                            | Alive; trust the skin, not the app                       |
| Seelen UI                | AGPL-3.0    | Free app; Seelen Sky subscription for extras | Built in plus custom                                      | Widgets load in an isolated webview but [call the app's commands directly](https://seelen.io/docs/widget-js-api); no per-widget grant                                                                                   | Alive, fast moving, rough edges                          |
| Themia                   | Proprietary | Free, or $19 Pro one-time                    | Vendor's own 22 widgets                                   | The [vendor's own widgets](https://themia.app/) only, no third-party code                                                                                                                                               | Alive, freemium, capped free tier                        |
| GadgetPack               | Freeware    | Free                                         | Windows 7 gadget platform                                 | Revives a platform Microsoft [advised disabling](https://support.microsoft.com/kb/2719662) over remote code execution                                                                                                   | Alive; for people who miss the old gadgets               |
| Widget Launcher          | Proprietary | Free, with in-app purchases                  | Vendor's own                                              | Third-party extensions run inside a [full-trust app](https://apps.microsoft.com/store/detail/9wzdncrdqfbt?hl=en-us\&gl=us), no permission model documented                                                              | Alive, easiest, ad-supported                             |
| BeWidgets                | Proprietary | Free on the Microsoft Store                  | Vendor's own                                              | The [vendor's own widgets](https://apps.microsoft.com/detail/9nq07fg50h2q) only, no third-party code                                                                                                                    | Alive, thin set                                          |
| XWidget                  | Proprietary | EUR 3.99 list on Steam                       | Widget server, reported down                              | JavaScript widgets from a gallery, [no updates since 2022](https://www.alternativeto.net/software/xwidget/about/)                                                                                                       | Avoid                                                    |
| Windows 11 Widgets Board | Proprietary | Free                                         | Packaged apps or PWAs, Adaptive Cards                     | [Declarative Adaptive Cards](https://learn.microsoft.com/en-us/windows/apps/design/widgets/) from packaged provider apps                                                                                                | Overlays the desktop, doesn't sit on it                  |

## What can a Rainmeter skin actually do on your PC? [#what-can-a-rainmeter-skin-actually-do-on-your-pc]

A skin is a plain-text .INI file, and that is why it looks harmless. Its reach is the part nobody mentions: **Rainmeter**'s [RunCommand plugin](https://github.com/rainmeter/rainmeter-docs/blob/master/source/manual/plugins/runcommand.html) "executes an external command or program in Windows", can start it with a hidden window, and asks for no per-skin permission. Whatever your Windows account can do, a skin can do, silently.

Rainmeter itself is clean; the question is every skin you download. Its own docs say most skin sites "have no policy of verifying either the identity of the uploader or the integrity of the files", tell you never to run a .exe that claims to be a skin installer, and send suspect files to [VirusTotal](https://github.com/rainmeter/rainmeter-docs/blob/master/source/manual/getting-started/customizing.html). Old skin sites are gone too, so people hunt for mirrors of unknown origin.

The other tools sit between those poles; the reach column in the table has each one. Seelen UI loads widgets into an isolated webview but lets them [call the app's commands directly](https://seelen.io/docs/widget-js-api). Widget Launcher runs third-party extensions inside a full-trust app. Themia and BeWidgets run only their own widgets. GadgetPack revives the platform Microsoft's advisory, ["Vulnerabilities in Gadgets could allow remote code execution"](https://support.microsoft.com/kb/2719662), told people to switch off. The Windows 11 Widgets Board accepts only declarative Adaptive Cards from [packaged apps](https://learn.microsoft.com/en-us/windows/apps/design/widgets/).

[DeskDash](https://www.desktop-dashboard.com) is the one tool in this set that makes you answer the question up front. Each widget runs in a [sandbox](https://www.desktop-dashboard.com/docs/concepts/security) with no direct reach into files, the network or other widgets. Before it runs, you see the capabilities it asks for and grant them per widget; the reference lists [what each permission unlocks](https://www.desktop-dashboard.com/docs/reference/permissions). API keys live in Windows Credential Manager, never inside a widget, and a misbehaving widget only affects itself.

![The DeskDash install prompt for the Weather widget: it asks for internet access, limited to api.open-meteo.com and geocoding-api.open-meteo.com, and for storage inside the dashboard, with an option to exclude permissions before installing.](/blog/deskdash-permission-prompt.png)

## Which Rainmeter alternatives are still maintained in 2026? [#which-rainmeter-alternatives-are-still-maintained-in-2026]

As of 26 August 2026, the maintained ones are **DeskDash**, **Seelen UI**, **Themia**, **GadgetPack**, **Widget Launcher** and **BeWidgets**; **XWidget** is discontinued.

DeskDash shipped Taskbars on 25 August 2026, its eighth release that month; the [changelog](https://www.desktop-dashboard.com/changelog) dates every version.

Seelen UI ships roughly weekly: v2.8.0, v2.8.1 and v2.8.2 landed between 20 July and 3 August 2026. It has [17.6k GitHub stars](https://github.com/eythaann/Seelen-UI), about three times Rainmeter's 6.0k, with a last push on 24 August 2026.

Themia is active: its pricing page is live and its blog carries posts dated 21 April and 10 August 2026. GadgetPack put out [version 42.0](https://gadgetpack.net/) on 11 May 2026. BeWidgets was updated on 12 March 2026, but Softpedia's users rate it 1.8 out of 5 against the editor's 4.0.

XWidget is [marked discontinued](https://www.alternativeto.net/software/xwidget/about/), last version October 2022. Its Steam score is Mixed, 66% positive from 409 reviews, and buyers report the developers have vanished.

The **Windows 11 Widgets Board** is maintained by Microsoft, which is walking back its defaults. Insider build 26300.8346, rolling out from 3 May 2026, turns off open on hover, taskbar badging, taskbar alerts and the MSN feed on first launch.

**Rainmeter** itself shipped three releases in May 2026 after a 13-month gap; the release before those, 4.5.23 in April 2025, fixed antivirus false positives. The 4.5.26 installer alone has been [downloaded 6.1 million times](https://github.com/rainmeter/rainmeter/releases/latest) from GitHub.

Directory pages won't tell you this. AlternativeTo lists [42 alternatives](https://alternativeto.net/software/rainmeter/), dead and living side by side, five of the top twelve not Windows tools at all.

## The alternatives, one by one [#the-alternatives-one-by-one]

### DeskDash [#deskdash]

DeskDash puts widgets and a taskbar on the wallpaper, behind your normal windows, in one install. You find, install, grant, update and remove widgets and [taskbars](https://www.desktop-dashboard.com/marketplace/taskbars) inside the app, from a [marketplace](https://www.desktop-dashboard.com/marketplace) where every listing is free and other authors publish beside the first-party ones; among the taskbars are [Dock](https://www.desktop-dashboard.com/marketplace/deskdash.dock), a macOS-style floating dock, and [Pulseglass Dock](https://www.desktop-dashboard.com/marketplace/kaladindmp.pulseglass-dock), a community taskbar by kaladindmp. The Windows taskbar comes back with one click. The widgets are interactive and keep their state: [Desktop Drawer](https://www.desktop-dashboard.com/marketplace/deskdash.desktop-drawer) turns any folder into an app drawer, [Todo](https://www.desktop-dashboard.com/marketplace/deskdash.todo) keeps the list you tick off, [Now Playing](https://www.desktop-dashboard.com/marketplace/deskdash.now-playing) controls the music, and [Steam Recent](https://www.desktop-dashboard.com/marketplace/deskdash.steam-recent) lists your last Steam games and launches one on click. Every widget is [sandboxed](https://www.desktop-dashboard.com/docs/concepts/security) and asks for [its capabilities](https://www.desktop-dashboard.com/docs/reference/permissions) before it runs. Widgets are plain HTML, CSS and JavaScript, no build step. Every display gets its own layout, and layouts come back when a display reconnects. It [runs on Windows 10 and 11](https://www.desktop-dashboard.com/download), 64 bit. One purchase, no subscription, and every widget and taskbar is included.

### Seelen UI [#seelen-ui]

Seelen UI adds a whole suite over Windows, not only widgets. It renders [widgets on the desktop](https://github.com/eythaann/Seelen-UI) (clocks, monitors, media controls, weather) and adds a themeable taskbar, a tiling window manager, a dock, a launcher and virtual desktops. The app is open source under AGPL-3.0 and free to install; a paid [Seelen Sky](https://seelen.io/sky) subscription, monthly or annual, adds cloud sync, exclusive widgets and themes, priority support and no ads on its website. [Open issues](https://github.com/eythaann/Seelen-UI/issues) include the toolbar not displaying, Win+L failing to lock, and Win+D hiding the dock and widgets on Windows 11 24H2. AlternativeTo reviewers give it [3.4 out of 5](https://alternativeto.net/software/seelen-ui/about/), call it "daunting", and complain that most information lives on Discord.

### Themia [#themia]

Themia is a polished widget set for Windows 10 and 11: Email, Calendar, Forecast, Folder and Notes plus [17 more](https://themia.app/). The free tier includes every widget type but caps you at 10 widgets and 2 screens on one device. Pro is [$19 one-time](https://themia.app/pricing), covers up to 5 devices and needs no account. The homepage claims it runs "without slowing things down, even on older laptops", a claim, not a measurement. Themia's own blog notes that for full sensor access, Rainmeter with HWiNFO [goes deeper](https://themia.app/blog/rainmeter-alternative-system-monitor/).

### GadgetPack [#gadgetpack]

GadgetPack installs the [gadget platform from Windows 7](https://gadgetpack.net/), patched for Windows 10 and 11, at version 42.0 since 11 May 2026. It is free, donation only. If you miss the old clock and CPU meter gadgets, this is the only tool that gives you exactly those.

### Widget Launcher [#widget-launcher]

Widget Launcher is a [free install](https://apps.microsoft.com/store/detail/9wzdncrdqfbt?hl=en-us\&gl=us) from the Microsoft Store. Pocket-lint notes it [shows brief ads](https://www.pocket-lint.com/apps-to-bring-widgets-to-windows-11-desktop/) when loading new widgets. The Store listing marks in-app purchases without showing their prices, and was last updated on 12 June 2026. XDA named it the ["easiest for beginners"](https://www.xda-developers.com/best-rainmeter-alternatives/). Fair enough: zero setup, ads as the price.

### BeWidgets [#bewidgets]

BeWidgets covers time, date, photos, finance and app shortcuts, last updated [12 March 2026](https://www.softpedia.com/get/System/OS-Enhancements/BeWidgets.shtml). The [Microsoft Store listing](https://apps.microsoft.com/detail/9nq07fg50h2q) is free, updated 2 April 2026, with no in-app purchases flagged. Softpedia users rate it 1.8 out of 5.

### uWidgets and WinWidgets [#uwidgets-and-winwidgets]

Two small open projects deserve a line. [uWidgets](https://github.com/creewick/uWidgets) is a free .NET 8 app with six widget types under CC BY-NC-SA 4.0. [WinWidgets](https://github.com/beyluta/WinWidgets) is GPL-3.0, has 153 stars, and builds widgets from HTML, CSS and JavaScript.

### XWidget [#xwidget]

XWidget is discontinued, last version October 2022. Steam still lists it at [EUR 3.99](https://store.steampowered.com/app/967130/XWidget/), but buyers report [the widget server is down](https://steamcommunity.com/app/967130/negativereviews/?browsefilter=toprated), the paid build duplicated the free download, and it looks pixelated on 4K. Don't pay for it.

### Windows 11 Widgets Board [#windows-11-widgets-board]

Microsoft defines the board as ["a flyout plane that overlays the Windows desktop"](https://learn.microsoft.com/en-us/windows/apps/design/widgets/), opened from the taskbar icon, Win+W or a swipe. It is free and already installed. It never puts anything on the desktop itself, though.

### Wallpaper Engine and Lively [#wallpaper-engine-and-lively]

Both do animated wallpapers and neither does widgets. Wallpaper Engine is listed at [EUR 4.99 on Steam](https://store.steampowered.com/app/431960/Wallpaper_Engine/), supports 3D and 2D animation, video and websites, and says nothing about widgets or monitoring. It has over a million reviews and an Overwhelmingly Positive rating. [Lively](https://github.com/rocksdanister/lively) is free, GPL-3.0, with 19.5k stars, and covers wallpapers and screensavers only. They are tools for motion; the rest of this list is about information on the desktop.

## What do the alternatives actually cost? [#what-do-the-alternatives-actually-cost]

**DeskDash** is a one-time purchase with every widget and taskbar included; **Rainmeter**, **GadgetPack** and **Lively** are free, and the rest are freemium or a one-time price.

One-time paid: DeskDash has no subscription, every widget and taskbar is free, and the [homepage](https://www.desktop-dashboard.com) shows the current amount. **Wallpaper Engine** is EUR 4.99, its list price, with no discount running on 26 August 2026. **XWidget** lists at EUR 3.99, but it is discontinued, so the price is moot.

Free, with no paid tier at all: Rainmeter, GadgetPack and Lively.

Freemium: **Seelen UI** is free to install, with the Seelen Sky subscription (monthly or annual) for cloud sync and its exclusive widgets and themes. **Themia** is free up to 10 widgets, then $19 one-time for Pro. **Widget Launcher** is free with in-app purchases. **BeWidgets** is free on the Microsoft Store.

## Where Rainmeter still fits [#where-rainmeter-still-fits]

If you want deep sensor data (voltages, NVMe temperatures, VRM current), **Rainmeter** paired with HWiNFO is still the best option, and a competing vendor [says so in print](https://themia.app/blog/rainmeter-alternative-system-monitor/). Nothing else on this list gets that far down.

Rainmeter's skin library is the largest in this list, and it holds [4.7 out of 5 on AlternativeTo](https://alternativeto.net/software/rainmeter/about/) with 517 likes; "there's something for everyone's taste". r/Rainmeter has about 323,000 members, growing around 1% a year.

Customising a skin means right-clicking it, choosing "Edit skin", and changing values in Notepad; a graphical settings panel exists only where a skin author [built one](https://github.com/rainmeter/rainmeter-docs/blob/master/source/manual/getting-started/customizing.html). One AlternativeTo reviewer had widgets disappear and could not get Rainmeter to run again; another says the layouts haven't been updated in a while.

Anyone running Rainmeter should follow its own rules: never run a .exe that claims to be a skin installer, check the uploader's history, and scan anything doubtful with VirusTotal.

## Which one should you pick? [#which-one-should-you-pick]

Pick **DeskDash** for interactive widgets and a replacement taskbar, each widget [sandboxed and granted its access](https://www.desktop-dashboard.com/docs/concepts/security) before it runs; the others fit narrower needs:

* DeskDash: widgets plus a replacement taskbar on the wallpaper, each widget sandboxed and asking for [its capabilities](https://www.desktop-dashboard.com/docs/reference/permissions) before it runs, one purchase with every widget and taskbar included.
* **Rainmeter**: deep sensor readouts through HWiNFO, the largest skin library, and skins that run with your full user rights.
* **Seelen UI**: open source, widgets plus a dock and a tiling window manager, a subscription for its exclusive widgets and themes, and open bugs in basic desktop integration.
* **Themia** and **Widget Launcher**: a small set of the vendor's own widgets; Themia caps its free tier at 10 widgets, Widget Launcher shows ads.
* **GadgetPack**: the Windows 7 gadgets, as they were.
* **Wallpaper Engine** and **Lively**: animated wallpaper, no widgets.
* **XWidget**: discontinued.

## Frequently asked

### What is the closest Rainmeter alternative for Windows 11?

**DeskDash**. It puts widgets on the desktop and replaces the taskbar, and it is the one where anyone can build a widget in plain HTML, CSS and JavaScript and share it through the marketplace.

### Which Rainmeter alternative has interactive widgets?

DeskDash. Its widgets take clicks and keep their state: Desktop Drawer turns any folder into an app drawer, Todo keeps the list you tick off, Now Playing controls the music, and Steam Recent launches your last Steam game. A Rainmeter skin can act on a click too, but the author hand-wires each action in the skin's .INI file.

### Is Rainmeter still maintained in 2026?

Yes. **Rainmeter** 4.5.26 shipped on 20 May 2026, the third release that month, after a gap of about 13 months.

### Is it safe to download Rainmeter skins?

Rainmeter's own docs say most skin sites verify neither the uploader nor the files, and tell you never to run a .exe skin installer and to scan doubtful files with VirusTotal. A skin can run any external program with a hidden window and no permission prompt.

### Which Rainmeter alternative is free and open source?

**Seelen UI** is open source under AGPL-3.0 and free to install, with its exclusive widgets and themes behind the paid Seelen Sky subscription. **GadgetPack** is free but not open source, and **Lively** is free under GPL-3.0 but does wallpapers, not widgets.

### Does Wallpaper Engine do widgets?

No. Its Steam page covers live wallpapers, animation, video and websites, and doesn't mention widgets or system monitoring.

### How much CPU or RAM does Rainmeter use?

No measured public figure exists. The question recurs on Hacker News and on Rainmeter's own forum without a number in reply.

# DeskDash widget docs (/docs)



DeskDash widgets are small web modules that live on your desktop: a folder of
plain HTML, CSS, and JavaScript running in a sandboxed frame, talking to the
system only through the `dd` SDK. There is no bundler, no framework, and no
build step. If you can write a web page, you can write a widget.

<Cards>
  <Card href="/docs/getting-started/what-is-a-widget" title="Getting started" description="The widget mental model, developer mode, and a first widget from scratch." />

  <Card href="/docs/guides/manifest" title="Guides" description="The manifest, settings, interactivity, popouts, data, components, theming, taskbars, and publishing." />

  <Card href="/docs/reference/api/core" title="Reference" description="The full dd.* API, manifest fields, permissions, tokens, components, events, and errors." />

  <Card href="/docs/concepts/security" title="How the sandbox works" description="The containment model behind widgets, and what install-time grants mean." />
</Cards>


# How the sandbox works (/docs/concepts/security)



Widgets are third-party code running on someone's desktop, so DeskDash is
designed as if every widget might be hostile. Not because yours is, but
because the guarantees users rely on have to hold for the worst widget on
the marketplace, not the average one. This page explains the layers, from
the frame outward, and what they mean for what your code can and cannot
do.

## The frame [#the-frame]

Every widget document runs in a sandboxed iframe that allows scripts and
nothing else. Without same-origin privileges, the frame gets an **opaque
origin**: no reaching into the surrounding app's DOM, no shared cookies or
storage with anything, and on modern runtimes its own renderer process.
The app's internal command channel is unusable from widget frames, so
there is no path from widget JavaScript to the host application, the
filesystem, or the OS. What is left inside the frame is a plain web page
that can compute and render.

## The network [#the-network]

Every response served to a widget document carries this content security
policy:

```
default-src 'none';
script-src http://widget.localhost;
style-src http://widget.localhost 'unsafe-inline';
img-src http://widget.localhost data:;
font-src http://widget.localhost data:;
connect-src 'none'; frame-src 'none'; object-src 'none';
base-uri 'none'; form-action 'none'
```

`connect-src 'none'` is the load-bearing line: no `fetch`, no XHR, no
WebSockets, from any widget, ever. Scripts load only from the widget's own
folder (so no remote code, no inline script, no eval), images and fonts
only from the folder or `data:` URIs, and no nested frames. Network
egress goes through the host only, two ways:
[`dd.http.fetch`](/docs/reference/api/data-services#ddhttp) runs against the
exact hostnames the manifest declared and the user consented to, re-validated
on every redirect hop, and
[`dd.feeds.fetch`](/docs/reference/api/data-services#ddfeeds) reads the feed
at an address the user typed into a `url` setting and returns only the parsed
items.

## Identity and the bridge [#identity-and-the-bridge]

Everything a widget asks of the system travels as a message to the shell,
and the shell knows who is asking from **which frame sent the message**,
never from anything inside it. Ids in a payload are ignored as identity;
a widget cannot claim to be another widget, because identity is not a
claim.

On the other side of that channel, every call is re-validated in the host:
the calling widget's granted permissions are checked, inputs are validated,
and failures come back as
[typed errors](/docs/reference/errors-versioning). The SDK is convenience,
not enforcement; the checks live host-side, so nothing a widget does in
JavaScript can skip them.

## User-granted, never widget-chosen [#user-granted-never-widget-chosen]

The pattern that repeats across every sensitive capability: &#x2A;*the widget
names things, the user supplies them, the host resolves them.**

* A `secretRef` setting names a credential. The user picks which stored
  secret it means, the host injects the value into the outgoing request
  after validating it, and the value itself never enters widget code, logs,
  or error messages.
* A `filePath` setting is a path only the user can fill in. Manifest
  defaults for these fields are deliberately ignored, so no widget can
  ship pre-pointed at anyone's files.
* A `url` setting is a web address only the user can fill in, with the same
  rule for defaults, so a widget never picks where the host connects. The
  host fetches the feed there and hands back titles, links and dates, never
  the page itself, and a redirect from a public address into a private
  network is refused. `feeds.open` opens only a link the host parsed out of
  that same feed, after a click inside the widget.
* Desktop and folder items are **opaque ids** issued by the host's own
  enumeration. A widget can open what the user put on their desktop and
  nothing else; it never handles a real path.
* Wifi connect works only for networks Windows already has a profile for;
  passwords never cross the bridge. System settings pages open by
  whitelisted key, never by arbitrary target.
* `links.open` accepts only URLs starting with a manifest-declared prefix,
  a host-side denylist of dangerous schemes applies no matter what the
  manifest says, and the call only works while the user's cursor is inside
  the widget.

## Install consent narrows the grant [#install-consent-narrows-the-grant]

At install time the user sees every requested permission in
[plain language](/docs/reference/permissions), with the `http` hosts and
`links` prefixes spelled out item by item, and can withhold any of them.
What they confirm is intersected with the manifest and enforced from then
on; a withheld capability behaves exactly as if the manifest never
declared it. The intersection only ever narrows (no stored state can
escalate past the manifest), a corrupted grant record fails closed to
deny-all, and an update that requests something new re-prompts for exactly
the additions.

Design consequence for authors: any call can come back `PERMISSION_DENIED`
even though your manifest declares it, because the user said no to that
part. Degrade the feature, not the widget.

## Input, popouts, and dialogs [#input-popouts-and-dialogs]

Mouse capture is decided by the host from where the user placed your
widget. A widget can narrow its capture area (that is what
[region marking](/docs/guides/interactivity) is), but it can never extend
it past its own rect. Cursor events stop at your own bounds too: no widget
observes the cursor anywhere else on the desktop.

The sanctioned ways to reach past your rect are all consent-shaped:
[popouts](/docs/guides/popouts) and host dialogs open only on a real
user gesture inside the widget, are size-capped by the manifest, and are
force-closed by the host the moment context changes. Dialogs go further:
the window is drawn by the host from host data, and the only thing that
returns to the widget is the shaped answer.

## The taskbar surface [#the-taskbar-surface]

A taskbar is a widget with `kind: "bar"`, and the surface it needs (the
open windows, the pinned apps, the strip, the preview flyout, the
notification area) is refused for every other kind before permissions are
even read. So a desktop widget can never see your open windows, whatever
its manifest declares, and the `taskbar` permission is meaningful only on
a bar. That grant is a read grant with one write: a reorder of the strip,
honored only while the cursor is over the bar.

Everything that crosses is an opaque host token: window ids, pin ids, icon
keys. Launching, pinning, closing a window and clicking a tray icon all run
host-side from a click inside a window the host draws (the preview flyout,
the per-app menu, the tray list). The bar says which app and where the
button is; what comes back is a count or an open state, never an icon, a
handle or a menu. The notification-area mirror arms only when a bar first
asks for it, so a bar that shows no tray reads nothing. The reference is
[bar widgets](/docs/reference/api/bar-widgets); the guide is
[building a taskbar](/docs/guides/taskbars).

## Between widgets [#between-widgets]

Widgets are mutually invisible. There is no widget-to-widget channel, no
shared storage, no way to enumerate what else is running. The two narrow
exceptions both run through the user:

* The `control` permission works only on instances the user explicitly
  ticked in that widget's settings, and instance settings are writable
  only through the host's own dialog. There is deliberately no
  `settings.set` in the SDK; adding one would let widgets approve
  themselves.
* Peer drag-and-drop works only between instances of the same widget type,
  the consent is the physical drag gesture itself, and the payload is
  carried and delivered by the host, not the DOM.

A hidden widget unmounts entirely, so nothing invisible keeps running or
listening. (A peek is the user's own gesture, not a widget's: it conceals
every widget at once and keeps them running, but they receive no input
while concealed.)

## What this means for you [#what-this-means-for-you]

Practically, the model gives you three habits:

* **Ask for the minimum.** Grants are visible, itemized, and refusable;
  small permission lists install better and review faster.
* **Design for denial.** Any gated call can reject; a native-feeling
  widget loses a feature, not its composure.
* **Never route around the SDK.** There is no other door, and anything
  that looks like one is a bug worth
  [reporting](mailto:contact@desktop-dashboard.com).

The flip side is what you gain: users can install your widget without
trusting you very much, and that is precisely what makes them willing to
install it.


# Developer mode and the dev kit (/docs/getting-started/developer-mode)



Developer mode is the switch between using DeskDash and building for it.
It lives in **Settings, General**: "Developer mode: tools for writing your
own widgets." It takes effect immediately, and it changes nothing about how
widgets run; it only reveals the tooling around them:

* the **Developer** settings page,
* **error badges** on widgets that report errors,
* **Inspect** in a widget's right-click menu.

Off (the default), none of that exists, so regular users never see a stray
badge or a debug menu.

## The Developer page [#the-developer-page]

Four sections, top to bottom:

**New widget.** A name and a folder id (auto-suggested from the name), and
**Create widget** writes a working widget folder plus its dev kit into your
widgets directory. That scaffold is the starting point of
[the tutorial](/docs/getting-started/your-first-widget). It is a desktop
widget; a [taskbar](/docs/guides/taskbars) starts from a copy of a bundled
bar in the taskbars folder, and &#x2A;*Add to a folder...** below writes its
dev kit.

**Widgets folder.** Shows where widgets load from, with **Reload** (the
same rescan as the tray's Reload widgets) and **Open** to jump there in
Explorer. The default location is `Documents\DeskDash\widgets`; changing it
is a general setting, under **Settings, General, Content**, since it moves
themes too.

**Dev kit.** **Refresh all** regenerates every widget's dev kit, and
&#x2A;*Add to a folder...** writes a kit into a widget folder you already have
(it must live in the widgets or taskbars folder and contain a
`manifest.json`).

**Debugging.** The **Developer tools** button, though the right-click
**Inspect** on a widget is usually the shorter path.

## What the dev kit is [#what-the-dev-kit-is]

The kit is a generated `.deskdash/` folder (plus one `jsconfig.json` beside
the widget) whose only job is making your editor understand the widget:

| File                       | What it gives you                                                                                              |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `deskdash.d.ts` and `sdk/` | the `dd` global: autocomplete for the whole SDK, hover docs, go-to-definition, and a typo is an error          |
| `settings.d.ts`            | `DdSettings`, generated from your own `manifest.json`, so your settings keys autocomplete with the right types |
| `dd-tokens.css`            | every `--dd-*` design token with its default value, for CSS autocomplete                                       |
| `manifest.schema.json`     | manifest completion and validation, wired by the `$schema` line the scaffolder writes                          |
| `jsconfig.json`            | ties it together; written only if absent, so your edits to it survive                                          |

Everything under `.deskdash/` is generated and disposable: nothing reads it
at runtime, deleting it changes nothing but your autocomplete, and
[published widgets](/docs/guides/publishing) leave it out entirely.

It stays fresh on its own where it matters: `settings.d.ts` regenerates on
every widget reload, so editing the manifest and reloading is enough to get
the new setting typed. The rest of the kit tracks the app, not your widget;
after a DeskDash update, press **Refresh all** once to pull the new SDK
types and tokens.

## The edit loop [#the-edit-loop]

There is no watcher and no build step; the loop is: edit, **Reload
widgets** (tray right-click, or the Developer page's Reload), look. The
reload rescans the widgets and themes folders and remounts every widget,
and widget files are served uncached, so what you saved is always what
runs.

When a widget throws an uncaught error or logs one through
`dd.log("error", ...)`, it keeps running but gains a red badge carrying the
message; the full text also lands in the app log with the widget's id. A
widget that stops responding entirely (a blocked main thread, an infinite
loop) is a different state: it gets replaced by a "stopped responding"
overlay, retried once automatically, then waits for a manual reload.

## Inspecting [#inspecting]

Right-click a widget, **Inspect**. Widgets are frames inside the desktop's
webview, so one inspector covers all of them: pick your widget's frame from
the console's context dropdown and you have its console, DOM, and network
view (which will be quiet, by [design](/docs/concepts/security)). CSS
changes made live in the inspector behave like in any browser; they last
until the next reload.


# What a widget is (/docs/getting-started/what-is-a-widget)



A DeskDash widget is a **plain web folder**. Nothing needs to be installed or built:

```
my-widget/
├─ manifest.json   # identity, permissions, settings, default size
├─ index.html      # the entry document
├─ widget.js       # plain JavaScript, classic or ES modules
└─ style.css       # styled with --dd-* tokens
```

The folder name must equal the manifest's `id`, and
[the manifest](/docs/guides/manifest) declares everything up front: what the
widget is, what it may access, and what users can configure. If you can
write a web page, you have all the skills already; the rest of these docs is
about what makes a widget different from a page.

## The frame it runs in [#the-frame-it-runs-in]

Each widget instance runs in a locked-down sandboxed frame with an isolated
origin. That single decision rules out a lot, on purpose:

* No network. `fetch`, XHR and WebSockets are blocked; remote images do not
  load.
* No browser storage: no `localStorage`, cookies, or IndexedDB.
* No reach into the app, other widgets, or the system.

Nothing your code can do escapes the frame. Every real-world capability
comes back in through one door, gated by the manifest. The full containment
story is on [how the sandbox works](/docs/concepts/security).

## The dd global: the single door [#the-dd-global-the-single-door]

The host serves an SDK script that defines a global `dd`. It is the entire
system surface: time, settings, storage, HTTP, system vitals, media, and
everything else flows through it, and every call is checked against the
permissions your manifest declared and the user granted.

```html
<script src="/__sdk/widget-sdk.js"></script>
<script src="widget.js"></script>
```

```js
await dd.ready;                      // nothing works until the host answers
dd.time.onTick(({ epochMs }) => {    // needs "time" in the manifest
  render(epochMs);
});
```

The SDK also registers the [`dd-*` components](/docs/guides/components) and
applies the user's theme to your document, which is why widgets follow
[the design tokens](/docs/guides/theming) for free.

## Instances on a grid [#instances-on-a-grid]

Users do not "run" your widget; they place **instances** of it on a grid
that spans their desktop. Sizes are measured in grid cells, your manifest
declares the default and minimum, and users resize freely from there,
including onto other monitors. The same widget can run as several instances
at once, each with its own settings and its own storage.

Two consequences worth absorbing early:

* Design for a range of sizes, not a fixed one. A widget that only looks
  right at its default size is half finished.
* Instance state is per instance. Two clocks can show two time formats; two
  weather widgets, two cities.

## The same folder, as a taskbar [#the-same-folder-as-a-taskbar]

A widget whose manifest says `"kind": "bar"` renders as the taskbar
instead of a tile on the grid: docked to a screen edge, one at a time,
with the Windows taskbar stepping aside while it runs. Same folder, same
sandbox, same SDK, plus a few bar-only components and a `taskbar`
permission. [Building a taskbar](/docs/guides/taskbars) covers it.

## Where widgets come from [#where-widgets-come-from]

Users install widgets from the built-in marketplace. Your own widgets live
as folders in the widgets directory (by default
`Documents\DeskDash\widgets`), which DeskDash scans at startup and on
reload; a folder there is a widget, immediately. Turning on
[developer mode](/docs/getting-started/developer-mode) adds the tooling
around that loop, and when a widget is ready to share,
[publishing](/docs/guides/publishing) takes it to the marketplace.

Ready to build one? [Your first widget](/docs/getting-started/your-first-widget)
goes from empty folder to a working clock in a few minutes.


# Your first widget (/docs/getting-started/your-first-widget)



This tutorial goes from nothing to a working clock on your desktop. You
need DeskDash running and
[developer mode turned on](/docs/getting-started/developer-mode); everything
else is a text editor.

## Scaffold it [#scaffold-it]

Open **Settings, Developer**. Under **New widget**, enter `My Clock` as the
name (the folder id auto-fills as `my-clock`) and press **Create widget**.
That writes a working widget folder into your widgets directory:

```
my-clock/
├─ manifest.json     # identity, permissions, settings, sizes
├─ index.html        # the widget's document
├─ widget.js         # the logic
├─ style.css         # the styling
├─ jsconfig.json     # editor wiring, written once, then yours
└─ .deskdash/        # generated types and schemas for your editor
```

The `.deskdash/` folder is what makes your editor smart here: typing `dd.`
autocompletes the whole SDK with docs, and the manifest validates as you
type. Nothing in it runs; it exists for you.

## Put it on the desktop [#put-it-on-the-desktop]

The new widget is already known to DeskDash. Enter edit mode from the tray
(**Edit layout**), open the marketplace from the edit controls and switch
to the **Installed** tab: `My Clock` is there. Add it, place it somewhere
comfortable, and leave edit mode. It says "Hello". Let's read why.

## Read what you got [#read-what-you-got]

The scaffolded manifest:

```json
{
  "$schema": "./.deskdash/manifest.schema.json",
  "id": "my-clock",
  "name": "My Clock",
  "version": "0.1.0",
  "apiVersion": 1,
  "description": "One line saying what this widget shows or does",
  "author": "",
  "permissions": {},
  "settings": [
    { "key": "title", "type": "text", "label": "Title", "default": "" },
    "panelStyle"
  ],
  "defaultSize": { "w": 4, "h": 2 },
  "minSize": { "w": 2, "h": 2 }
}
```

`permissions` is empty: a fresh widget can do nothing beyond rendering.
It declares one text setting plus the shared panel-style preset, and its
sizes are in grid cells. There is no `minAppVersion`: when you publish, the
store works out which DeskDash release the widget needs and stamps it for
you. Every field is covered in [the manifest guide](/docs/guides/manifest).

The document is two components inside a panel, then the SDK and your
script:

```html
<body>
  <dd-panel id="panel" settings="panelStyle">
    <dd-text slot="heading" id="title"></dd-text>
    <dd-text variant="value" id="value">--</dd-text>
  </dd-panel>

  <!-- The SDK first: it defines the dd-* elements and the `dd` global. -->
  <script src="/__sdk/widget-sdk.js"></script>
  <script src="widget.js"></script>
</body>
```

And the script wires values to those elements with signals:

```js
const { signal, effect } = dd.ui;

const titleEl = document.getElementById("title");
const valueEl = document.getElementById("value");

const settings = signal(/** @type {DdSettings} */ ({}));
const value = signal(null);

effect(() => {
  const title = settings.value.title ?? "";
  titleEl.textContent = title;
  titleEl.hidden = !title;
});

effect(() => {
  valueEl.textContent = value.value == null ? "--" : String(value.value);
});

async function main() {
  // Nothing on `dd` is usable until the host has answered.
  await dd.ready;

  // Fires once now, and again whenever the user changes a setting.
  await dd.settings.bind((next) => {
    settings.value = next;
  });

  value.value = "Hello";
}

// Boot errors are reported to DeskDash rather than thrown. With developer
// mode on you will see them on the widget itself.
main().catch((err) => {
  dd.log("error", "my-clock failed to start", String(err));
});
```

Three habits in there are the house style: everything waits on `dd.ready`,
settings arrive through `dd.settings.bind` (now and on every change), and
the boot `catch` routes errors to the host instead of letting them vanish.

`DdSettings` is the fourth. It is a type generated from your own
`manifest.json`, so `settings.value.` offers the settings you declared and
flags the ones you did not. It is regenerated every time widgets reload,
which means the manifest stays the single place you describe them.

`style.css` opens by importing the tokens:

```css
@import "./.deskdash/dd-tokens.css";
```

That is editor food, not styling: type `--dd-` and your editor lists every
design token with its value. DeskDash injects the user's active theme at
runtime and that always wins, so the import can never fight a theme. Below
it, one trick worth seeing: the readout is sized with `min(16vw, 40vh)`, and
since the widget is its own little document, `vw` and `vh` mean the widget's
own box. The number scales with the tile.

## Make it tick [#make-it-tick]

A clock needs the time, and the time is a capability. In `manifest.json`:

```json
"permissions": { "time": true },
```

Then in `widget.js`, replace the `value.value = "Hello";` line with a
subscription to the host's tick stream, plus a seed so it does not sit
empty for the first second:

```js
let lastEpochMs = 0;

function render() {
  if (!lastEpochMs) return;
  value.value = new Date(lastEpochMs).toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });
}

// inside main(), after settings.bind:
dd.time.onTick(({ epochMs }) => {
  lastEpochMs = epochMs;
  render();
});
const now = await dd.time.now();
lastEpochMs = now.epochMs;
render();
```

Never build your own one-second timer; the host's tick is second-aligned
and shared by every widget that needs it.

Now the loop you will live in: right-click the tray icon and pick **Reload
widgets**. The desktop remounts your widget, permission and all, and the
clock ticks. Every edit from here is save, reload, look.

## Add a setting [#add-a-setting]

Give users a say. In the manifest's `settings` array, next to the title
field:

```json
{ "key": "showSeconds", "type": "boolean", "label": "Show seconds", "default": true },
```

And make `render()` respect it:

```js
function render() {
  if (!lastEpochMs) return;
  value.value = new Date(lastEpochMs).toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: settings.value.showSeconds ? "2-digit" : undefined,
  });
}
```

Also call `render()` from the `settings.bind` callback, so a change applies
instantly instead of on the next tick. Reload, enter edit mode, and open
the widget's settings from its gear: your toggle is there, rendered by the
host, and it previews live as you flip it. (After the reload, the generated
types know about `showSeconds` too; `settings.value.` autocompletes it.)

## When something breaks [#when-something-breaks]

Break it on purpose: misspell `getElementById` and reload. The widget gets
a red error badge with the message, and the full line lands in the app log.
Right-click the widget and pick **Inspect** for the browser developer
tools; your widget is a frame you select from the console's context
dropdown. Fix the typo, reload, badge gone.

## The finished widget.js [#the-finished-widgetjs]

```js
const { signal, effect } = dd.ui;

const titleEl = document.getElementById("title");
const valueEl = document.getElementById("value");

const settings = signal(/** @type {DdSettings} */ ({}));
const value = signal(null);
let lastEpochMs = 0;

effect(() => {
  const title = settings.value.title ?? "";
  titleEl.textContent = title;
  titleEl.hidden = !title;
});

effect(() => {
  valueEl.textContent = value.value == null ? "--" : String(value.value);
});

function render() {
  if (!lastEpochMs) return;
  value.value = new Date(lastEpochMs).toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: settings.value.showSeconds ? "2-digit" : undefined,
  });
}

async function main() {
  await dd.ready;

  await dd.settings.bind((next) => {
    settings.value = next;
    render();
  });

  dd.time.onTick(({ epochMs }) => {
    lastEpochMs = epochMs;
    render();
  });

  const now = await dd.time.now();
  lastEpochMs = now.epochMs;
  render();
}

main().catch((err) => {
  dd.log("error", "my-clock failed to start", String(err));
});
```

About forty lines, and it is a real widget: permission-gated, settable,
theme-following, and honest about errors.

## Where to go next [#where-to-go-next]

* [Writing the manifest](/docs/guides/manifest), for everything the
  manifest can declare.
* [Using the components](/docs/guides/components), to build richer UI the
  house way.
* [Working with data](/docs/guides/working-with-data), when the widget
  should show something from the outside world.
* [Interactivity](/docs/guides/interactivity), when it should respond to
  clicks.
* [Design guidelines](/docs/native/guidelines), to make it feel native
  before you [publish it](/docs/guides/publishing).


# Using the components (/docs/guides/components)



The SDK registers a set of `dd-*` Web Components that carry the house style:
`dd-panel` for the surface, `dd-text` for any text, `dd-icon`, `dd-button`,
`dd-icon-button`, `dd-chip`, `dd-meter`, `dd-app-tile`, and `dd-volume-pill`.
Reach for them before raw divs and classes: they come pre-styled from the
design tokens, follow every theme for free, and keep widgets looking like
one family. Attributes, parts and slots for each are in
[the component reference](/docs/reference/components).

One behavioral fact shapes how you use them: the components are
**presentational**. A click on a `dd-button` bubbles from the host element
and nothing happens until you wire it; `el.onclick`, `dd.peers.draggable(el)`
and friends attach to the host like to any element. The single exception is
`dd-volume-pill`, which drives the system volume itself.

## Structure stays in HTML [#structure-stays-in-html]

Someone reading `index.html` should be able to see what the widget is: its
shape, its regions, its parts. The Clock's whole body:

```html
<dd-panel id="panel" settings="panelStyle">
  <dd-text variant="value" id="time" data-dd-hover>--:--</dd-text>
  <dd-text variant="muted" id="date"></dd-text>
</dd-panel>
<script src="/__sdk/widget-sdk.js"></script>
<script src="widget.js"></script>
```

That is the whole widget, structurally. Build DOM in JavaScript only for
structure that is genuinely dynamic: lists generated from data, or a region
whose shape changes with state. A fixed layout with changing values is not
dynamic structure; leave it in HTML and update the values.

## Signals and effects [#signals-and-effects]

For those changing values, the SDK ships a small reactive core:
`dd.ui.signal`, `computed`, and `effect`. Declare state as signals, bind
each to its static element with one small effect, and write to `.value`
from wherever the data arrives. The Weather widget's entire render layer:

```js
const { signal, effect } = dd.ui;

const temp = signal("--°");
const condition = signal("");
const place = signal("");

effect(() => (tempEl.textContent = temp.value));
effect(() => (conditionEl.textContent = condition.value));
effect(() => (placeEl.textContent = place.value));
```

After this, "rendering" is assignment: `temp.value = "21°"` updates exactly
the one element that reads it. `computed` derives values that update with
their inputs; System Controls turns one vitals snapshot into per-concern
slices with it:

```js
const vitals = signal(null);
const wifi = computed(() => (vitals.value ? vitals.value.wifi : null));
const muted = computed(() => !!(vitals.value?.volume && vitals.value.volume.muted));
```

Do not over-reach, though. If a handful of values always change together
from one callback, plain imperative updates in a `render()` function are
fine; the Clock works that way. Signals earn their keep when many pieces
change independently.

## bindParts for reactive attributes [#bindparts-for-reactive-attributes]

When the changing values are attributes spread through a subtree, inline SVG
being the classic case, one effect per attribute gets noisy. Keep the shape
in HTML, mark only the changing nodes with `data-bind`, and declare all the
bindings in one call. The Status Bar's battery glyph:

```html
<span id="battery">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
    <rect x="3.2" y="8.4" width="14.6" height="7.4" rx="2"/>
    <rect data-bind="level" x="4.7" y="10.2" height="3.9" fill="currentColor" stroke="none"/>
    <path data-bind="bolt" d="M11.6 9.3 9.8 12.2h3.2l-1.8 2.9"/>
  </svg>
</span>
```

```js
dd.ui.bindParts(batteryEl, {
  level: {
    width: () => Math.max(0.5, (percent.value / 100) * 11.6).toFixed(1),
    opacity: () => (charging.value ? "0" : "1"),
  },
  bolt: { opacity: () => (charging.value ? "1" : "0") },
});
```

The fill bar narrows with charge and swaps opacity with the bolt while
charging, and no code ever rebuilds the SVG. The props take the same
reactive functions `create()` does.

## create() for structure that varies [#create-for-structure-that-varies]

`dd.ui.create` builds elements, and any prop given as a function becomes a
live binding. Use it for the regions whose structure genuinely varies, and
for stateful art whose node count depends on data. System Controls builds
its wifi button once, with every dynamic attribute bound:

```js
const wifiBtn = create(
  "dd-icon-button",
  {
    round: true,
    active: () => activePane.value === "wifi",
    off: () => !wifi.value,
    title: () => (wifi.value ? `${wifi.value.ssid} (${wifi.value.signal}%)` : "Wifi: not connected"),
    onClick: () => flyout.toggle("wifi"),
  },
  wifiIcon(),
);
```

For a list, build the rows inside an explicit effect and swap them in with
`replaceChildren`. The wifi network list from the same widget:

```js
const netListEl = create("div", { class: "netlist dd-scroll" });

effect(() => {
  const nets = networks.value;
  if (nets === null) {
    netListEl.replaceChildren(create("div", { class: "dd-empty" }, "Scanning..."));
  } else if (nets.length === 0) {
    netListEl.replaceChildren(create("div", { class: "dd-empty" }, "No networks found"));
  } else {
    netListEl.replaceChildren(...nets.map(netRow));
  }
});
```

## The one rule [#the-one-rule]

Per subtree, pick one strategy: **build once with reactive bindings**, or
**wholesale re-render** on change. Never put reactive bindings inside rows
that a re-render throws away. In the list above, `netRow` builds plain rows
with no bindings, because the effect discards them on every change; the
wifi button, built once, is all bindings. Mixing the two in one subtree
leaks work and confuses ownership. (The SDK auto-disposes bindings of
removed elements as a backstop, but that is a backstop, not a design.)

So the division of labor across a widget reads: static shape in HTML,
changing values via `effect` or `bindParts`, and only the genuinely varying
region built and swapped with `create`.

## Reaching inside a component [#reaching-inside-a-component]

Component internals live in a Shadow DOM, so your CSS cannot restyle them
with ordinary selectors. That is what keeps them theme-correct. When you do
need to adapt one, use the exposed hooks, in escalating order: `::part()`
selectors, the documented custom-property knobs, named slots, and finally
subclassing via `dd.ui.components` with a `super` call to keep the original
behavior. All four are documented per component in
[the reference](/docs/reference/components), with a subclass example. If a
component will not expose what you need, hand-roll that one piece rather
than fighting it.

```css
/* the usual case: restyle the panel surface via its part */
#panel::part(surface) {
  flex-direction: row;
  gap: var(--dd-space-4);
}
```

## Shared patterns worth knowing [#shared-patterns-worth-knowing]

A few house patterns ship as plain classes and helpers rather than
components:

* **`.dd-status`**: the muted "source / age / error" line pinned to a
  panel's bottom edge. Put `dd-text` elements inside; it hides itself while
  they are all empty. The two type steps that go on those `dd-text`s are
  `.dd-dim` (0.85em, muted) and `.dd-warn` (the warning color), so a status
  line needs no CSS of its own.
* **`.dd-empty`**: the centered "nothing here" placeholder, as in the list
  example above.
* **`.dd-autoscale`**: the readout-scales-with-the-widget skeleton, covered
  in [theming](/docs/guides/theming#autoscale-readouts-that-follow-the-widgets-size).
* **`dd.ui.pressGuard(el)`**: start a click handler with
  `if (!dd.ui.pressGuard(btn)) return;` to swallow double activations
  instead of hand-rolling a disable-and-timeout dance.


# Interactivity and input (/docs/guides/interactivity)



Widgets sit on the desktop, and the desktop has to stay usable. So the
resting state of every widget is **click-through**: the mouse passes
straight to the icons and windows behind it. Interactivity is an opt-in, and
even then, input is captured only where and when it should be.

## Opting in [#opting-in]

```json
"interactive": true
```

With that manifest flag, the host captures the mouse while the cursor is
over your widget and releases it the moment it leaves. Inside, you get real
mouse events: CSS `:hover`, tooltips, clicks, wheel. The rect is padded by a
small ring (about 12 px) so fast approaches do not slip a click through, and
capture decisions freeze while any mouse button is held, so a drag never
loses the widget mid-gesture.

Without the flag, the widget is a ghost: it renders, it can even react to
the cursor (see below), but every click lands on the desktop.

## Marking what is solid [#marking-what-is-solid]

By default an interactive widget captures its whole rectangle. Most widgets
should narrow that: mark the elements that should feel solid with
`data-dd-interactive`, and the SDK measures them and reports the regions to
the host automatically. Everything unmarked stays click-through.

```html
<!-- whole-panel: the entire bar is solid -->
<dd-panel id="bar" row data-dd-interactive>
  <div id="iconbar"></div>
</dd-panel>
```

```html
<!-- per-control: only the transport is solid, clicks elsewhere pass through -->
<dd-icon-button name="skip-back" data-dd-interactive></dd-icon-button>
<dd-icon-button id="play" name="play" data-dd-interactive></dd-icon-button>
<dd-icon-button name="skip-forward" data-dd-interactive></dd-icon-button>
```

Rows built in JavaScript mark themselves the same way, as a prop:
`create("div", { class: "row", "data-dd-interactive": "", onClick: ... })`.

Mark **everything that should be solid**, not just the clickable bits. The
marked regions are what your widget owns: they define where clicks stick,
and when widgets overlap they also define where your pixels show (more on
that below). Unmarked gaps fall through, clicks and all.

The measuring is fully managed: the SDK re-measures on DOM changes, on
resize, and when CSS transitions and animations end, then reports up to 32
regions. There is a `dd.input.setRegions` call underneath, but you never
call it yourself; the attribute is the API.

## Mark after layout [#mark-after-layout]

One real trap: an element marked in static HTML gets measured at boot,
before your data has arrived and before layout has settled. If that
boot-time rect is wrong, a sliver of capture can stick where nothing
visible is. The Desktop Drawer had exactly this, and its fix is the pattern:

```js
function markRegions() {
  headerEl.setAttribute("data-dd-interactive", "");
  contentEl.toggleAttribute("data-dd-interactive", !collapsed);
}

async function main() {
  await dd.ready;
  // ...load data, render, settle layout...
  markRegions();   // only now: the panel has its real size and state
}
```

Two things to copy: regions are marked only after the first real layout,
and they narrow with state (the drawer's board is only solid while
expanded; collapsed, it has slid away and marking it would swallow desktop
clicks over the vacated area). Until you mark anything, the SDK falls back
to whole-rect capture, which is never too small; late marking is safe,
early marking is not.

## Hover without capture [#hover-without-capture]

Two mechanisms give you cursor awareness with zero input capture, so even
ghost widgets can react:

* **`data-dd-hover`** on an element: the SDK toggles a `dd-hover` class on
  it while the cursor is over it. Pure styling, no subscription.
* **`dd.cursor.onHover`** for the whole widget. The Clock, which captures
  nothing, styles itself on approach:

```js
dd.cursor.onHover(({ inside }) => {
  document.body.classList.toggle("hovered", inside);
});
```

```css
body.hovered #panel:not([transparent]):not([bare])::part(surface) {
  border-color: var(--dd-accent);
}
```

Note what those `:not()`s are for. A `::part()` rule in your stylesheet wins
over the component's own styling, so an unscoped hover rule paints a border
(or a background) back onto a panel whose owner chose **Transparent** or
**No panel**. The [`panelStyle` preset](/docs/reference/components#settings-presets-on-components)
puts those two attributes on the host for exactly this reason: scope any
panel styling that fights the user's choice.

There is also `dd.cursor.onMove` for a stream of positions while inside;
see [the environment reference](/docs/reference/api/environment#ddcursor).

## Gestures and guards [#gestures-and-guards]

* A press-drag that may leave your marked regions (a slider, a drag
  handle) should take `setPointerCapture` on pointerdown, so the gesture
  survives leaving the rect. If the drag is a
  [peer drag](/docs/reference/api/widgets-together#ddpeers),
  `dd.peers.draggable` already handles the whole threshold and capture
  dance.
* Start click handlers with `if (!dd.ui.pressGuard(btn)) return;` where a
  double activation would double-act (launching apps, toggling hardware).

## When widgets overlap [#when-widgets-overlap]

Widgets may overlap freely, so the input rules extend to a stacking story:

* The click goes to the **top-most widget with solid area under the
  cursor**, where solid means its marked regions (or the whole rect if it
  marked nothing).
* Interactive widgets rank above ghosts. A click-through widget parked on
  top of a working one never steals its clicks or its hover.
* A widget covering another is **clipped to its marked regions**, paint and
  hit-testing both: through your unmarked gaps, the widget underneath is
  visible and clickable. Marking honestly is what makes overlap composable.
* Right-clicking still reaches a widget over its unmarked area (for its
  context menu), preferring solid regions when stacked. A widget can opt
  out with `"contextMenu": false` in its manifest (app 0.2.3+): right-clicks then pass through it in view
  mode, to a widget beneath or to the desktop's own menu. Edit mode
  ignores the flag, so the widget stays configurable and removable. See
  [the manifest reference](/docs/reference/manifest).

## Bar widgets [#bar-widgets]

Bar-kind (taskbar) widgets are the opposite default: the bar window takes
the mouse natively everywhere, with no `interactive` flag and no marking.
For a solid full-width bar that is the whole story.

A bar with empty space (a centered dock pill, split segments) opts into
click-through by wrapping its visible content in
[`<dd-bar>`](/docs/reference/components#dd-bar) segments. Each segment's box
captures the mouse; everything outside every segment clicks through to the
desktop or the maximized window beneath. Under the hood this is the same
region machinery as above (the segment marks itself `data-dd-interactive`),
with two bar-specific twists: regions are fractions of the whole bar
window, and the host keys them per display window, since the same bar
document runs on every display. The first non-empty report enrolls the
window; reporting nothing keeps native input, so an unmarked bar behaves
exactly as before.

Pointer events outside the captured segments never reach the document.
Hang pointer machinery (hover waves, leave timers) on the segment itself,
not on a full-window wrapper. Pair segments with the manifest's
`bar.reserve` knob (a reserved strip smaller than the window height) and
maximized windows fill up behind the transparent parts of the bar.

## Panes are different [#panes-are-different]

Popout pane documents never mark regions: a pane captures its whole rect
for as long as it is open, by design. Put `data-dd-interactive` only in the
main document. The same applies to `dd.input.setRegions` being
main-document only; a pane has no say in capture.


# Writing the manifest (/docs/guides/manifest)



`manifest.json` sits at the root of the widget folder and declares everything
the host needs to know before it runs a line of your code: who the widget is,
which API it speaks, what it may access, what the user can configure, and how
big it starts. Nothing is inferred; if it is not in the manifest, the widget
does not have it.

## The starting point [#the-starting-point]

Scaffolding a widget (or adding the dev kit to a folder) gives you this
manifest:

```json
{
  "$schema": "./.deskdash/manifest.schema.json",
  "id": "bus-times",
  "name": "Bus Times",
  "version": "0.1.0",
  "apiVersion": 1,
  "description": "One line saying what this widget shows or does",
  "author": "",
  "permissions": {},
  "settings": [
    { "key": "title", "type": "text", "label": "Title", "default": "" },
    "panelStyle"
  ],
  "defaultSize": { "w": 4, "h": 2 },
  "minSize": { "w": 2, "h": 2 }
}
```

Every line of it earns its place, so the rest of this page walks through the
fields in the order you will care about them. The exhaustive field tables live
in [the manifest reference](/docs/reference/manifest).

## Identity [#identity]

`id` is the widget's stable identity: lowercase letters, digits, dots and
dashes, starting with a letter or digit, and it must equal the folder name.
The host treats a mismatch as an error, because the id doubles as the URL
path your files are served from.

While you develop, the id is simply yours to pick. When you publish, the
store assigns the public id itself: it becomes `yourhandle.widget-name`,
derived from the widget's name, and whatever id you wrote locally is
replaced. Details in [the publishing guide](/docs/guides/publishing).

`name` is the display name users see everywhere (and, on publish, the source
of that id slug). `description` is one plain sentence shown in listings.
`author` is informational locally; the store overwrites it with your
publisher handle.

## The three versions [#the-three-versions]

Three fields carry version meaning, and they gate different things:

* **`version`** is yours. The app treats it as an opaque label, but the
  marketplace requires dotted numbers (`1.2.3` style) and every update must
  carry a strictly higher one, so use orderable numbers from day one.
* **`apiVersion`** names the bridge contract the widget was written against.
  It is `1` today and changes rarely; the host refuses manifests newer than
  itself and renders a needs-an-update tile for widgets older than its
  supported floor. The lifecycle is on
  [errors and versioning](/docs/reference/errors-versioning).
* **`minAppVersion`** (optional) is a feature floor: "this widget needs
  DeskDash x.y.z or newer". An older app shows a clean "needs DeskDash x.y.z
  or newer" message instead of a confusing failure. You normally leave it
  out: the store stamps every published version with the floor its manifest
  needs, and a value you write only counts when it is higher. How the floor
  is worked out is
  [in the reference](/docs/reference/errors-versioning#minappversion-the-feature-floor).

## The entry document [#the-entry-document]

`entry` names the HTML document the widget boots from. It defaults to
`index.html`, so most manifests omit it. If you set it, it must be a plain
relative path inside the widget folder: no leading slash, no `..`, no
backslashes. The same rules apply to `popout.entry` if the widget declares
[a popout](/docs/guides/popouts).

## Sizing on the grid [#sizing-on-the-grid]

Widgets live on a grid, and `defaultSize` and `minSize` are measured in grid
cells, not pixels: `{ "w": 4, "h": 2 }` is four cells wide, two tall.
`defaultSize` is what a fresh instance gets; `minSize` is the smallest the
user can resize it to. `defaultSize` accepts 1 to 64 per axis.

`minSize` accepts 0.25 to 64 per axis, in steps of 0.25, so a slim strip
can go below one cell on a finer grid. Users pick a grid scale of Normal,
Half or Quarter, and the app rounds your floor up to whole cells of that
grid, never below one cell. A `minSize` height of `0.25` is one cell on the
Quarter grid and one cell on Normal; `0.5` is two cells on Quarter. A
fraction is app 0.3.9+.

How many pixels a cell is depends on the user's monitor and their grid
density setting, so never design for an exact pixel size. Design for the
range instead: make `minSize` the smallest shape the widget is genuinely
useful at, and test what it looks like both there and much larger.
[Theming and tokens](/docs/guides/theming) covers the autoscale helpers that
make readouts follow the widget's size.

## The schema line [#the-schema-line]

```json
"$schema": "./.deskdash/manifest.schema.json"
```

The [dev kit](/docs/getting-started/developer-mode) generates a JSON Schema
describing the whole manifest format, and this line points your editor at it.
With it in place you get autocompletion for every field, inline validation
while you type, and hover documentation. The host ignores the field, so it is
safe to keep in published widgets even though the generated `.deskdash/`
folder itself does not ship.

## Permissions and settings [#permissions-and-settings]

The two biggest sections of a real manifest each have their own guide:

* `permissions` declares every capability the widget needs; everything is
  deny-by-default and users consent at install time. See
  [the permission reference](/docs/reference/permissions) for the full list.
* `settings` declares the fields users edit in the widget's settings dialog,
  including shared presets like `"panelStyle"`. See
  [settings and presets](/docs/guides/settings).

## Strict on purpose [#strict-on-purpose]

The manifest parser rejects unknown fields instead of skipping them. That
sounds harsh, but it means a typo like `"defualtSize"` fails loudly at scan
time with a message naming the field, rather than silently doing nothing and
leaving you staring at a widget that ignores its configured size.

## Two real manifests [#two-real-manifests]

The Clock widget is about as small as a useful manifest gets. Note the
`settings` array mixing full field objects with the `"panelStyle"` preset
name:

```json
{
  "id": "clock",
  "name": "Clock",
  "version": "0.1.0",
  "apiVersion": 1,
  "description": "Time and date, driven by the host's tick source",
  "author": "deskdash",
  "permissions": { "time": true },
  "settings": [
    { "key": "format24h", "type": "boolean", "label": "24-hour clock", "default": true },
    { "key": "showSeconds", "type": "boolean", "label": "Show seconds", "default": true },
    { "key": "showDate", "type": "boolean", "label": "Show date", "default": true },
    "panelStyle"
  ],
  "defaultSize": { "w": 6, "h": 3 },
  "minSize": { "w": 3, "h": 2 }
}
```

System Controls shows the two opt-ins Clock does not need: `interactive`
(the widget captures mouse input; see
[interactivity](/docs/guides/interactivity)) and `popout` (a second document
it may open; see [popouts](/docs/guides/popouts)):

```json
{
  "id": "system-controls",
  "name": "System Controls",
  "version": "0.1.1",
  "apiVersion": 1,
  "description": "Wifi, volume and battery in one bar: network list with connect/disconnect, working volume slider, settings shortcuts",
  "author": "deskdash",
  "permissions": {
    "system": true,
    "systemControls": true
  },
  "interactive": true,
  "popout": { "max": { "w": 4, "h": 4 }, "entry": "pane.html" },
  "settings": [
    { "key": "showWifi", "type": "boolean", "label": "Show wifi", "default": true },
    { "key": "showVolume", "type": "boolean", "label": "Show volume", "default": true },
    { "key": "showBattery", "type": "boolean", "label": "Show battery", "default": true },
    "panelStyle",
    "accentColor"
  ],
  "defaultSize": { "w": 4, "h": 1 }
}
```

No `minSize` here means it falls back to the default minimum of one cell.


# Popouts and flyouts (/docs/guides/popouts)



A widget's main document never resizes: its rect is the user's layout
decision. When a widget needs more room temporarily (a network list, a
volume slider, a detail view), it opens a **popout**: a second sandboxed
document from the same widget folder, running with the same grants, overlaid
next to the widget at a rect the host grants. Close it, and the desktop is
exactly as before.

## Declaring it [#declaring-it]

```json
"popout": { "max": { "w": 4, "h": 4 }, "entry": "pane.html" }
```

`entry` names the pane's HTML document (same path rules as the main entry),
and `max` caps the pane's size in grid cells (required for a grid widget;
a taskbar omits it, see [On a taskbar](#on-a-taskbar)). Without this
manifest block, `dd.popout.open` is not available.

## The pane document [#the-pane-document]

The pane is a full document with its own markup and script. System
Controls' pane body, in full:

```html
<div id="root" class="pane-root"></div>
<script src="/__sdk/widget-sdk.js"></script>
<script type="module" src="pane.js"></script>
```

No `data-dd-interactive` anywhere: a pane captures its whole rect for as
long as it is open, so the marking machinery from
[interactivity](/docs/guides/interactivity) does not apply inside it.

The pane's `dd.ready` context tells it why it exists: `ctx.popout.data` is
whatever `open()` sent, and `ctx.popout.side` is the side of the widget the
host actually placed it on. System Controls uses both, picking what to build
from the data and matching its entrance animation to the side:

```js
async function main() {
  const ctx = await dd.ready;
  const { pane, font } = (ctx.popout && ctx.popout.data) || {};

  // pane content scales with the em base the opener sent along
  document.documentElement.style.fontSize = `${Number(font) > 0 ? Number(font) : 14}px`;

  const root = document.getElementById("root");
  root.className = `pane-root dd-anim-slide-${ctx.popout.side}`;
  root.append(BUILDERS[pane]());
}
```

One pane document serving several roles through `data`, as here, beats
declaring nothing and guessing.

## Opening [#opening]

From the **main document only**:

```js
await dd.popout.open({
  size: { w: 320, h: 240 },      // your own CSS pixels; granted exactly, up to the manifest max
  anchor: wifiBtn,               // an Element: the SDK measures it for you
  prefer: "down",                // "down" | "up" | "left" | "right" | "auto"
  data: { pane: "wifi" },        // lands in the pane's ctx.popout.data
});
```

The host grants the size exactly (within the manifest cap) and places the
pane on the preferred side when it fits, flipping when it does not; the
resolved side comes back to both documents. Size and anchor are in your
widget's own pixels, the ones you measure inside your document: below a
Widget size of 100% the host lays your document out larger than its tile
and scales it down, and it converts a popout request at that same boundary,
so the pane paints at your widget's scale and the granted size comes back
in your pixels. The manifest cap is a screen ceiling, so a
widget painted small may ask for more of its own pixels before it bites. Opening is consent-gated: it
works only while the user's cursor is inside the widget, and never in edit
mode. In practice that means popouts open from clicks and hovers, not from
timers; a widget cannot pop UI at an unsuspecting desktop.

`dd.popout.close()` closes from either document and is fine to call when
nothing is open.

## On a taskbar [#on-a-taskbar]

A `kind: "bar"` widget declares the same block without `max` and calls the
same `dd.popout.open` from a click. The host renders the pane in its own
window beside the bar's strip (above a bottom bar, to the side of a left
or right bar), sized to your CSS px and capped to the display; `prefer` is
ignored, the pane always opens away from the bar. The pane document gets
the bar's grants, `ctx.popout.side` (the direction it opened toward) and
`ctx.bar.edge` (the edge the bar docks to). Consent is the cursor over the
bar; from the pane, the gesture verbs (`dd.settings.open`, `dd.links.open`,
`dd.dialogs.pickDesktopItems`, `dd.power.run`) pass while the cursor is over
the pane or the button that opened it (app 0.3.7 or newer). Dismissal is the
app menu's: Escape, or a press outside the pane and
the button that opened it; the host also closes it on a fullscreen app, a
reload, a tray-list click or a taskbar change, so follow `onChange` for
your pressed state. The pane window never takes keyboard focus, so design
it pointer-only. `send`/`onMessage` work between the two documents.

## Messaging [#messaging]

The two documents are isolated frames; the SDK relays messages between
them. `dd.popout.send(payload)` in one document, `dd.popout.onMessage(cb)`
in the other, both directions, with a size cap per message (see
[the reference](/docs/reference/api/widgets-together#ddpopout)).

One relay contract is load-bearing enough to call out: a `dd-volume-pill`
in a pane emits `dragchange` while its slider is being dragged, and the
pane should forward it so the main document's flyout helper knows not to
retract mid-drag:

```js
pill.addEventListener("dragchange", (e) => dd.popout.send(e.detail));
```

## Lifecycle: design for force-closes [#lifecycle-design-for-force-closes]

`dd.popout.onChange(({ active, side }) => ...)` fires in both documents on
open, reshape, and close. Closes are not always yours: the host force-closes
a popout when the user enters edit mode, on widget reload, on a grid
change, when the instance is hidden, disabled, or removed, and when either
document crashes.

The design consequence: a pane holds no state the main document cannot
rebuild, and reopening after any close must just work. Treat the pane as a
projection, keep the source of truth in the main document (or in
[`dd.storage`](/docs/guides/working-with-data#small-persistent-state)), and
send it across on open via `data`.

## Flyouts: the transient-pane helper [#flyouts-the-transient-pane-helper]

Most popouts are the same pattern: click a trigger, a small pane slides out
anchored to it, and it should retract when the cursor leaves or the trigger
is clicked again. `dd.popout.flyout` packages that pattern, owning the
open-and-close choreography: mirroring host closes, hover-out retraction,
holding open during a slider drag (the `dragchange` relay above), and
rolling back cleanly when an open fails.

The Status Bar's volume flyout is the minimal case, one pane and two lines
of wiring:

```js
const flyout = dd.popout.flyout({
  panes: {
    sound: { size: (font) => ({ w: 20 * font, h: 3.4 * font }), anchor: () => volumeEl },
  },
});

volumeEl.addEventListener("click", () => flyout.toggle());
```

System Controls scales the same shape to three panes behind three buttons,
with the pane font following the bar's autoscaled size, and its buttons
highlighting from the state callback:

```js
const flyout = dd.popout.flyout({
  panes: {
    wifi:    { size: (font) => ({ w: 20 * font, h: 18 * font }),  anchor: () => wifiBtn },
    sound:   { size: (font) => ({ w: 20 * font, h: 3.4 * font }), anchor: () => soundBtn },
    battery: { size: (font) => ({ w: 16 * font, h: 3.4 * font }), anchor: () => batteryBtn },
  },
  font: () => paneFont(unit()),
  onState: (active) => { activePane.value = active ?? "none"; },
});
```

Sizes are functions of the pane's em base, so panes scale with the widget
instead of arriving at one fixed size. Clicking an open pane's trigger
closes it; clicking another switches panes. The full option surface is in
[the reference](/docs/reference/api/widgets-together#ddpopoutflyout).

## Raw open or flyout? [#raw-open-or-flyout]

Use `flyout` whenever the pane is an anchored transient that should retract
on its own; that is nearly every popout, and the helper's edge cases (drag
guards, host closes, failed opens) are exactly the ones widgets get wrong
by hand. Drop to raw `open`/`close` when the pane is a deliberate,
persistent companion panel the user dismisses explicitly, or when you need
pixel-rect anchoring the element form does not express.


# Publishing to the marketplace (/docs/guides/publishing)



Publishing puts your widget in the in-app marketplace, where anyone can
install it with one click after reviewing its permissions. The pipeline is
deliberately simple on your side: zip the folder, fill in a listing, submit,
and a human reviews it.

## Before you start [#before-you-start]

Two prerequisites, both one-time:

* **A purchased DeskDash account.** Submissions are tied to paid accounts.
* **A publisher handle**, set from your account page on the website: 3 to 30
  characters, lowercase letters, digits and dashes, starting with a letter
  or digit. Choose carefully; the handle is permanent, it becomes part of
  every widget id you publish, and it is shown as the author on your
  listings.

## What you upload [#what-you-upload]

A zip of the widget folder, from the Submit page on desktop-dashboard.com.
Zipping the folder itself or just its contents both work. The caps:

* 3.5 MB for the zip
* 200 files, 5 MB per file, 12 MB total unpacked

OS junk (`.DS_Store`, `Thumbs.db`, macOS resource folders) is dropped
automatically, and so are the generated `.deskdash/` folder and
`jsconfig.json`. Those are development files: your editor reads them, a
published widget does not need them (the app serves the `dd-tokens.css`
import path itself, so a scaffolded stylesheet keeps working without the
folder). Zip them or leave them out as you like. They never count against
the limits above, and they never reach the people who install your widget.

## Taskbars [#taskbars]

A taskbar (a widget with `kind: "bar"`, see
[building a taskbar](/docs/guides/taskbars)) ships through this same
pipeline. Pick **Taskbar** at the top of the submit page before you
upload: the zip's manifest must declare `kind: "bar"` with its `bar` block,
and the choice is checked against the zip, so a mix-up gets a clear error instead of a mislabeled listing.
Taskbars have no store categories. Screenshots show the bar on a desktop
edge, at a size where its icons are readable. Everything else on this page
applies as written, and the published taskbar appears on the
[Taskbars shelf](/marketplace/taskbars) and under Taskbars in the app's
marketplace, where installing it offers **Use** right away.

## What validation checks [#what-validation-checks]

Upload validation enforces the same manifest rules the app does, strictly.
The ones that catch people:

* Unknown manifest fields are errors, not warnings.
* `version` must be dotted numbers, like `1.2.3`.
* `minAppVersion`, if you write one, must be a plain `x.y.z`. You do not need
  one: see [the app version](#the-app-version) below.
* Font files are checked by content, not extension: woff2 only, and every
  font file must be declared in `fonts.bundle`.
* `settings` holds at most 32 entries; a preset name counts as one.

A failed upload returns the full list of problems; nothing is submitted
until the zip is clean.

## The app version [#the-app-version]

You do not pick which DeskDash release your widget needs. The store works it
out from the manifest and stamps it as `minAppVersion` on every new version:
each permission, setting type, preset and manifest field you use has a
release it last changed in, and so does the SDK itself, and the stamp is the
newest of those. The upload's success screen and your submissions page show
the result ("needs DeskDash 0.3.9 or newer").

A `minAppVersion` you write yourself only counts when it is higher, for the
rare widget that depends on an app fix. A published version keeps the value
it shipped with. People on an older app see "Needs app update" on your
listing.

## Your widget's public identity [#your-widgets-public-identity]

This is the part that surprises people, so read it twice: &#x2A;*the store
assigns your widget's id.** At first publish, the id becomes
`yourhandle.widget-name`, with the second half derived from the manifest's
`name`. Whatever `id` you used locally is replaced, and the `author` field
is stamped with your handle. Both are outside your control by design; ids
carry the publisher's identity, so no one can squat a name or impersonate
another author.

Two consequences:

* The id is minted once. Renaming the widget in a later update changes its
  display name everywhere, but never the id.
* If you publish a second widget whose name slugs to the same id, the
  upload is rejected and asks you to rename or update the existing widget.

## The listing [#the-listing]

The listing is filled in on the submit form, not shipped in the zip:

* **Category**, from the store's fixed set.
* **Feature points**: 1 to 8 short lines, up to 120 characters each. These
  render as the listing's bullet list; make them concrete ("Connect and
  disconnect wifi networks", not "Powerful networking").
* **Long description**: up to 2000 characters, shown below the features.
* **Screenshots**: 1 to 5, ordered; the first is the cover shown in the
  store grid.

The listing title and short description always come from the manifest's
`name` and `description`, so those two fields are worth polishing before
you zip.

## Screenshots [#screenshots]

Still PNG, JPEG, or WebP images, up to 4 MB each, checked by content.
There is no enforced size or aspect ratio; the practical guidance is the
form's own: show the widget as it looks on a desktop, at a size where its
text is readable. Each screenshot takes optional alt text (up to 160
characters) for accessibility.

One thing to know before you upload: screenshots become reachable at a
public URL as soon as they are staged, which is before review. Do not
include anything in a screenshot you would not want public.

## Review [#review]

Every submission goes through automated checks and then a human review of
the actual code. The reviewer reads every file, so the bar is legibility:

* Permissions must be justified by what the widget visibly does. The
  smaller the grant list, the faster the review; `http` hosts and `links`
  prefixes get particular attention.
* No minified or obfuscated code. Widgets ship as plain source with no
  build step, and the review depends on that.
* Bundled fonts get a licensing check; include the license and make sure
  the font actually permits redistribution.

Your submissions page on the website shows each submission's status, and
you get an email at each decision:

* **In review**: waiting for the reviewer.
* **Changes requested**: the reviewer wants something fixed, and the note on
  the page (and in the email) says what. Use **Upload a fix** there: keep the
  same `version`, and the fix joins the same review instead of starting a new
  one.
* **Live**: accepted and published.
* **Not accepted**: the note says why. Nothing was published, so you may
  submit again with the same version number.
* **Withdrawn**: you took it back. Any open submission has a **Withdraw**
  button.
* **Failed**: a pipeline error on the store's side.

One submission per widget can be open (in review or waiting for a fix) at a
time.

## After acceptance [#after-acceptance]

The version is published and becomes **immutable**: its exact bytes are
what users install, verified against a checksum, and no one, you included,
can replace the contents of a published version. Fixes ship as new
versions.

## Shipping an update [#shipping-an-update]

Updates are explicit: on the submit page, choose the widget under "Update
an existing widget" and upload the new zip.

* The manifest `version` must be strictly higher than the published one,
  and a published version number is never reused. (After a rejection or a
  withdrawal you may resubmit the same number; nothing was published.)
* The listing form prefills from the current listing but is applied
  **wholesale**: what the form contains on submit is the whole new listing,
  so an emptied field disappears from the store page when the update lands.
* The category is fixed on version updates; change it through a listing
  edit instead.
* If the update adds a permission, users are asked again at install and
  update time; removed permissions apply silently. Growing the grant list
  is friction for every existing user, so treat additions deliberately.

## Listing-only edits [#listing-only-edits]

Screenshots, features, description or category changes without a code
change do not need a version bump: your published widgets' pages have an
"Edit listing" action that submits just the listing for review, no zip
involved.

## Unlisting a widget [#unlisting-a-widget]

Your submissions page has an **Unlist** button on each live widget. An
unlisted widget leaves the marketplace for new users: it drops out of
browse, search, its web page and the sitemap. Everyone who already installed
it keeps it, still gets the updates you publish, and can reinstall it.
**Relist** puts it back with no new review, since its content was reviewed
when it shipped. The widget id stays yours either way.

Unlisting never uninstalls copies people have, and deleting your account
does not remove your published widgets either. To have a widget removed for
everyone, contact support.


# Settings and presets (/docs/guides/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()`](/docs/reference/api/settings-storage#ddsettingsopen).

## Declaring fields [#declaring-fields]

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

```json
"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`](/docs/reference/api/environment#dddatesfromdaykey)
  (`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+):

```json
{ "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](/docs/guides/working-with-data#secrets-in-requests).
The full field tables are in
[the manifest reference](/docs/reference/manifest#settings). A bar manifest
may not declare these three types at all; see
[the taskbars guide](/docs/guides/taskbars#settings-without-an-instance).
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 [#presets-shared-fields-shared-behavior]

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

```json
"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](/docs/reference/api/settings-storage#ddimageload); you
  write zero code:

  ```html
  <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`*&#x2A; and &#x2A;*`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](/docs/reference/components#dd-apps-and-dd-app).

* **`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](/docs/reference/api/environment#dddatesstartofweek),
  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](/docs/reference/api/ui#dduiautofit): 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 [#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:

```js
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 [#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 [#reacting-to-changes]

What "react" means depends on the setting:

* Values that feed a fetch: name them in your
  [`dd.poll`](/docs/guides/working-with-data#polling-with-ddpoll) 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](/docs/guides/components#signals-and-effects) 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:

  ```js
  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.


# Building a taskbar (/docs/guides/taskbars)



A DeskDash taskbar is a widget with `"kind": "bar"`. The same folder of
plain HTML, CSS and JavaScript, the same sandbox, the same `dd` SDK, the
same components and tokens as a [desktop widget](/docs/getting-started/what-is-a-widget).
What changes is the surface: instead of a tile on the grid, the widget
renders as the taskbar, docked to a screen edge, and the Windows taskbar
steps aside while it runs.

## What the host does for you [#what-the-host-does-for-you]

The split of ownership is the whole model, so read it first.

The host owns the window. It creates one bar window per display, docks it
to the edge your manifest names, reserves the strip with the OS so
maximized windows stop above it, hides the native Windows taskbar and
brings it back on quit, on a crash, or when the user turns the bar off.
Your document never moves, never resizes and never knows about any of it.

The host owns the data. One ordered strip of pinned and running apps, the
same for every bar on every display; the open windows, already cut to
what each bar shows; the pins, which
live in the app config and survive a bar switch; the notification area,
read back from Windows so a bar can offer it. A bar renders from these
streams and never keeps an order or a pin list of its own.

The host owns every popup. Window previews, the per-app right-click menu,
the tray list and your own pane are separate host-drawn windows that open
beside the strip. A bar says when and for which app; the host draws,
places, refreshes and closes.

You own the HTML and CSS. That is where two taskbars differ: the dock is a
centered pill with a magnification wave, the rail is a tall glass column
on a side edge, aurora is a full-width bar with a clock and a now-playing
chip. Behind all three sit the same four components and the same store.

One bar runs at a time. The user picks it in the marketplace, and it shows
on every display, or on the one they choose in **Settings, Taskbar** (app
0.3.4+). With a bar on every display, **Show taskbar apps on** in the same
page picks which bar lists a window, like the Windows setting of that
name (app 0.3.11+). The host applies it to the window list, so your bar
needs no code for it. Bars have no instances: the document's `role` is `"bar"` and its
`instanceId` is the sentinel `"__bar"` on every display.

## Start from a folder [#start-from-a-folder]

Bars load from the `taskbars` folder, next to the widgets folder (default
`Documents\DeskDash\taskbars`). The split is enforced both ways: a
`kind: "bar"` manifest in the widgets folder is quarantined with a pointer
at the move, and only bar widgets load from the taskbars folder. The two
folders share one id namespace.

The scaffolder in [developer mode](/docs/getting-started/developer-mode)
makes desktop widgets, so a taskbar starts by hand: copy one of the bundled
bars from the taskbars folder, or create a folder with the files below.
&#x2A;*Add to a folder...** on the Developer page writes the dev kit into it.

```
my-bar/
├─ manifest.json   # kind, the bar block, permissions, settings
├─ index.html      # the bar document
├─ style.css       # the look
└─ widget.js       # optional: only what the look needs
```

The manifest of the bundled dock, which is a complete taskbar:

```json
{
  "id": "dock",
  "name": "Dock",
  "version": "0.4.5",
  "apiVersion": 1,
  "description": "A macOS-style floating dock ...",
  "author": "deskdash",
  "kind": "bar",
  "bar": { "thickness": 128, "reserve": 68 },
  "settings": ["pins", "systray"],
  "permissions": { "taskbar": true }
}
```

Four things are bar-specific. `kind` makes it a taskbar. The `bar` block
sizes the window (the [manifest reference](/docs/reference/manifest#bar)
has every field and range). The `taskbar` permission unlocks the bar-only
surface: windows, pins, the strip, previews, the tray and the app menu
(see [permissions](/docs/reference/permissions)). The two
[presets](/docs/reference/manifest#setting-presets), `pins` and `systray`,
give the user the "Show pinned apps" and "Show the system tray" toggles
that the components apply on their own. There is no `minAppVersion`: the
store stamps it when you publish (older apps do not know `kind`, and the
strip and drag to reorder are app 0.3.5+); the
[feature floor](/docs/reference/errors-versioning#minappversion-the-feature-floor)
explains how.

## Three tags are a taskbar [#three-tags-are-a-taskbar]

This is the dock's document, with its comments and ids trimmed:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="/__sdk/dd-base.css" />
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <dd-bar place="center" reach="35">
      <dd-panel row>
        <dd-bar-button action="start" label="Start"></dd-bar-button>
        <dd-apps settings="pins" indicator="dots" sortable></dd-apps>
      </dd-panel>
      <dd-tray-caret settings="systray" hidden></dd-tray-caret>
    </dd-bar>
    <script src="/__sdk/widget-sdk.js"></script>
  </body>
</html>
```

Every behavior a taskbar has is in those tags, with no widget code:

* [`<dd-bar>`](/docs/reference/components#dd-bar) is a segment: the part
  of the window that takes the mouse. Everything outside it clicks through
  to the desktop or the maximized window beneath. `place` says where along
  the edge it sits, `reach` adds hover headroom above the content.
* [`<dd-bar-button action="start">`](/docs/reference/components#dd-bar-button)
  is the start button. It draws the four-pane logo itself and taps the
  Windows start menu on a click. `action="quick-settings"` and
  `action="notifications"` open Windows' own Quick Settings and
  notification center the same way (app 0.4.2 or newer), so your users
  keep the sound device, Bluetooth and Night light with no code of yours.
* [`<dd-apps>`](/docs/reference/components#dd-apps-and-dd-app) is the app
  strip: one `<dd-app>` per row of the host's strip, pins and running apps
  in the user's order, windows merged per app. A click launches, focuses,
  minimizes or opens the preview flyout; a right-click opens the host's
  per-app menu; `sortable` (app 0.3.5+) makes a press-and-drag reorder the
  strip. `settings="pins"` wires the "Show pinned apps" toggle.
* [`<dd-tray-caret>`](/docs/reference/components#dd-tray-caret) is the
  notification-area caret. It stays hidden until the tray has icons and
  the setting is on, and a click opens the host-drawn tray list.
  `settings="systray"` wires the toggle.
* Not in the dock, but one tag away:
  [`<dd-notification-bell>`](/docs/reference/components#dd-notification-bell)
  (app 0.4.2 or newer) opens the host's own notification center beside
  your bar, with the count as its badge, instead of Windows' one in the
  corner. `settings="notificationCenter"` wires a "Show notifications"
  toggle.

The dock's `widget.js` exists only for the magnification wave. A bar that
wants a different look edits `style.css`; the components are the box, so
size them, animate them and style their parts from widget CSS. The
[components guide](/docs/guides/components) has the general rules and the
[reference](/docs/reference/components#dd-apps-and-dd-app) lists every
attribute, state and part.

## See it [#see-it]

Open the marketplace (the tray icon, or &#x2A;*Add widgets...** in its menu),
switch to **Installed**, then **Taskbars**: your folder is listed with a
**Use** button, because one bar runs at a time. Use it, and the Windows
taskbar gives way to yours.

Right-click the bar for its menu. &#x2A;*Taskbar settings...** opens the bar's
own settings dialog: the **Placement** group first (Side, when the
manifest lists several edges; Size, 50% to 200%; Auto-hide; Let windows go
behind the bar), then your own settings, live-previewed as they change.
The same dialog has a **Style** tab (also &#x2A;*Taskbar style...** in the menu):
the user's `--dd-*` overrides for the bar alone, background, border, fonts
and colors, previewed live through `theme.tokens` like a theme switch. Your
CSS sees a different `var()` value and nothing else changes.
&#x2A;*Change taskbar...** goes back to the Taskbars section. After an edit,
**Reload widgets** from the tray reloads the bar too.

Placement is the user's. The manifest tunes what they can pick: `edges`
lists the sides the bar supports, `reserve` and `allowOverlay` decide
whether windows may fill behind it, and the optional `autoHide` block sets
the feel of auto-hide (the delay, the reveal band on the screen edge, the
slide time). The user switches auto-hide on; you say how it behaves. The
slide is the host's: your document neither moves nor needs to know, and
the reserved strip goes to zero while auto-hide is on.

## Lay out along the edge [#lay-out-along-the-edge]

A bottom bar is the full display width and `thickness` tall. A left or
right bar is the full display height and `thickness` wide, and every host
popup opens to its side instead of above it. Your document learns the edge
from `ctx.bar.edge` in the [init context](/docs/reference/api/core#ddready)
(the same value reaches a bar's pane); a `<dd-bar>` segment reads it on its
own and lays out as a column on a side edge, and `dd.bar.store().edge`
carries it as a signal. The rail mirrors its layout with one attribute on
the body:

```js
const ctx = await dd.ready;
document.body.dataset.edge = ctx.bar?.edge ?? "bottom";
```

Size against the viewport, not in fixed pixels. The user's **Size**
slider (app 0.3.1+) scales the window's thickness, and with it the
reserved strip, so a document whose sizes derive from `100vh` (a bottom
bar) or `100vw` (a side bar) scales whole, while literal pixel sizes and
theme font-size tokens do not. The bundled bars use one unit:

```css
:root {
  --u: calc(100vh / 128);            /* the manifest thickness, in design px */
  --icon-rest: calc(var(--u) * 52);  /* an icon at rest */
  --pill-h: calc(var(--u) * 72);
}
```

Treat the manifest `thickness` as the design size, never as a constant.
Colors, radii and type come from the [design tokens](/docs/guides/theming)
like everywhere else.

## Input: segments, not flags [#input-segments-not-flags]

A bar has no `interactive` flag. The window takes the mouse natively
everywhere: plain CSS `:hover` and click handlers just work, and for a
solid full-width bar that is the whole story.

A bar with empty space (a centered pill, split segments) wraps its visible
content in `<dd-bar>` segments. Each segment's box captures the mouse;
everything outside every segment clicks through. Two consequences:

* Pointer events outside the segments never reach your document, so hang
  hover waves and leave timers on the segment, not on the body.
* A `reserve` smaller than `thickness` leaves transparent headroom above
  the strip where maximized windows fill in. Use it only together with
  segments; without them that band is an invisible click-eater. Give the
  segment `reach` so hover animations that rise into the headroom stay
  hoverable.

The mechanics, and the rules for marking regions after layout, are in the
[interactivity guide](/docs/guides/interactivity#bar-widgets).

## Previews, the app menu and the tray are the host's [#previews-the-app-menu-and-the-tray-are-the-hosts]

A stacked app's click opens the preview flyout: window titles and live
snapshots, drawn by the host beside the strip. `previews="hover"` on
`<dd-apps>` opens it after a dwell instead, the native taskbar feel. The
look is tunable through a curated style (preview width, radius, gap,
surface and border colors, title and close button), set once with
`dd.bar.store().setFlyoutStyle(style)`; everything else about the flyout
is the host's, including when a card shows the app's icon instead of a
snapshot. The [flyout reference](/docs/reference/api/bar-widgets#ddbarflyout)
has the ranges and the fallbacks.

A right-click on an app button opens the host's per-app menu: launch, pin
or unpin, close. `<dd-app>` tags itself so the SDK raises it; a bar that
draws its own buttons stamps the same tag by hand
([dd.bar.appMenu](/docs/reference/api/bar-widgets#ddbarappmenu)). Never
draw an app menu or keep a pin list of your own: the actions are host
privileges, the pins belong to the app config, and a bar is too thin to
hold a menu anyway. Untagged space raises the bar's own menu.

The tray caret opens the host's tray list. A bar sees a count and the
open state, never the icons, so the caret's pressed look follows the
[dd.systray](/docs/reference/api/bar-widgets#ddsystray) stream. Tell your
users what the list shows: every app's icon, hidden ones dimmed, and
DeskDash's own; the Windows volume, network and battery icons stay with
Windows.

## A pane of your own [#a-pane-of-your-own]

A bar may declare the same `popout` block as a grid widget, without
`max`, and open it from a click. The host renders the pane document
beside the strip, in its own window, sized to your CSS px and capped to
the display. The rail's games button is one:

```html
<dd-bar-button action="pane" pane-w="620" pane-h="620" label="Games">
  <dd-icon src="gamepad.svg" tint></dd-icon>
</dd-bar-button>
```

```json
"popout": { "entry": "games.html" },
"permissions": { "taskbar": true, "games": true }
```

The pane gets the bar's grants and `ctx.bar.edge`, closes on Escape, an
outside press, a fullscreen app or a taskbar change, and never takes
keyboard focus, so design it pointer-only. The taskbar streams reach the
pane too. What each document may call, from app 0.3.7:

* Bar window only: the flyout, the app menu, the tray list, the strip
  move, the start-menu tap, `dd.popout.open` and the region reporting.
* Bar window or pane, under the cursor-inside gate (the bar window, or the
  pane and its anchor button): `dd.settings.open` (it opens the Taskbar
  settings window), `dd.links.open`, `dd.dialogs.pickDesktopItems` and
  `dd.power.run`. Older apps refuse these from a bar outright.
* Both, with no gate: the streams, `dd.bar.strip.list`, the `dd.windows`
  reads and `dd.pins.list`.

Details in [popouts, on a taskbar](/docs/guides/popouts#on-a-taskbar); the
games library behind that pane is
[dd.games](/docs/reference/api/desktop-apps).

## Settings without an instance [#settings-without-an-instance]

Bars have no instance, so the active bar has one settings home in the app
config, served by `dd.settings.get` merged over your manifest defaults,
and one dialog: the bar's right-click &#x2A;*Taskbar settings...**. Values
live-preview as they change, Save persists, any other close replays the
stored values. Everything in the [settings guide](/docs/guides/settings)
applies, with two exceptions. A bar manifest may not declare an explicit
consent-bearing field (`secretRef`, `filePath`, `instances`): the bar dialog
never offers those types, so the app refuses the manifest at load, the upload
refuses it and the CI check fails it (app 0.3.7 or newer); preset names are
exempt, so `panelStyle` still works minus its `image` option. And the two bar
presets, `pins` and `systray`, exist for bars only.

## Storage without an instance [#storage-without-an-instance]

[`dd.storage`](/docs/reference/api/settings-storage#ddstorage) on a bar
addresses one store that belongs to the bar widget, not to an instance. The
bar document on every display and the bar's pane read and write the same
keys, under the same limits as a grid widget. Another bar never sees it, and
it is still there when the user switches back to yours.

Nothing announces a write. The copy of your bar on another display learns of
it the next time it reads, so read the value at the moment you need it
instead of caching it at boot.

This needs app 0.4.1 or newer. An older app rejects every storage call a bar
makes with `INVALID_PARAMS`, and `dd.weather.geocode` with it, because its
cache is a storage read.

## Every other grant works [#every-other-grant-works]

`taskbar` unlocks the bar-only surface. The rest of the
[permission set](/docs/reference/permissions) (`time`, `system`, `media`,
`audio`, `notifications`, `http` and the others) is declared and gated on a
bar exactly as on a grid widget, and the streams reach the bar document on
every display the bar runs on. Aurora is the worked example: a clock, a
now-playing chip, vitals rings and an audio-reactive surface beside its
apps, plus a transport deck in its pane. One note for `audio`: capture
stays demand-driven, the active bar counts as a visible audio widget, and
its `anyAudio` setting is read from the bar's settings.

## What stays native, and the limits [#what-stays-native-and-the-limits]

The Windows taskbar is hidden, not removed, and Windows keeps reporting it
to other apps. Your users will see three things beside a side bar or a
short bar: the Start menu opens where Windows puts it, at the bottom edge,
left or center per their Windows setting, and Quick Settings and Windows'
own notification center in the bottom right corner (the
`<dd-notification-bell>` center opens beside your bar instead); third-party apps that place
their toasts above "the taskbar" (Steam, for one) keep a gap at the
bottom; and Explorer re-shows the hidden taskbar on every Start open and
work-area change, which the host re-hides at once, so a flash is the most
anyone sees. A bar that wants Start beside itself draws its own start pane
with the pane mechanism above.

Elevated windows (Task Manager, an elevated terminal, some installers) sit
behind a Windows boundary that drops messages from ordinary apps. Focus
still works through a fallback; minimize is refused and the click does
nothing; the close request from a preview card or the app menu is dropped;
the preview shows the icon tile. Previews also fall back to the icon tile
for minimized windows and for windows that draw straight to the GPU
(games, some browsers and players). None of this is yours to fix, and the
streams never desync over it: drive every indicator from
`dd.windows.onChanged`, never from a click.

## Publish [#publish]

Taskbars ship through the same marketplace pipeline as widgets. On the
[submit page](/marketplace/submit), pick **Taskbar** at the top and upload
the folder as a zip; the manifest must declare `kind: "bar"` with its
`bar` block, and the choice is checked against the zip so a mix-up gets a clear error instead of a
mislabeled listing. Taskbars have no store categories; everything else
(screenshots, features, the long description, the review) works exactly
like a widget submission, see [publishing](/docs/guides/publishing).
Published taskbars appear on the store's
[Taskbars shelf](/marketplace/taskbars) and in the app's marketplace under
Taskbars, where installing one offers **Use** right away.

## Where to go next [#where-to-go-next]

* [Bar widgets reference](/docs/reference/api/bar-widgets): the store, the
  strip, the windows, the pins, the flyout, the tray and the app menu.
* [Components](/docs/reference/components#dd-bar): every attribute, state
  and part of the four bar components.
* [A taskbar that feels native](/docs/native/guidelines#a-taskbar-that-feels-native):
  the corner, the order, the motion.


# Theming and tokens (/docs/guides/theming)



Every color, size, font, radius, and duration in DeskDash flows from the
`--dd-*` design tokens. When the user switches themes, the host rewrites the
token values on your document root and the whole desktop restyles in one
paint, widgets included, with no reloads. Your side of that bargain is
simple: **style with the tokens, never with literal values**. A hardcoded
`#3b82f6` looks fine on your wallpaper today and wrong on every theme you
did not test.

The complete token list with defaults is
[the token reference](/docs/reference/tokens). This page is about using them
well.

## Tokens from CSS [#tokens-from-css]

Plain `var()` everywhere a visual value appears:

```css
.forecast-row {
  gap: var(--dd-space-2);
  border-bottom: 1px solid var(--dd-border);
  color: var(--dd-text-muted);
}
```

For text, you rarely even write CSS: `<dd-text variant="title">`,
`size="xl"`, `weight="semibold"` and friends map onto the type tokens for
you. The one sanctioned escape is a truly special size, like a hero readout
scaled to the widget box, where a plain `font-size` on that element (via an
id or class selector) overrides the variant default:

```css
#time {
  font-size: min(19vw, 52vh);  /* the Clock's hero digits */
}
```

Viewport units are your own widget box, so this still scales with the
instance; it just is not a theme concern.

## Tokens from JavaScript [#tokens-from-javascript]

CSS resolves `var()` for you; canvas drawing and dynamically built inline
SVG cannot. For those, read the computed value with `dd.ui.token` and re-read
it on theme changes, never caching across one:

```js
let accent = dd.ui.token("--dd-accent", "#888888");

dd.theme.onChange(() => {
  accent = dd.ui.token("--dd-accent", "#888888");
  draw();                       // repaint with the new palette
});
```

`dd.theme.onChange` runs once a theme's colors have finished fading in (app
0.4.0+), so the value you read there is the new color, not a step of the fade.
The fallback is only for a token that does not resolve at all; never treat
it as the real color. The Slot Machine widget's confetti works exactly this
way: the burst colors are read from tokens at draw time and re-read from
`dd.theme.onChange`, so even a canvas animation follows the theme.

You do not need any of this for plain CSS. The `theme.tokens` update is
delivered to every widget and the SDK re-applies the variables to your
document root automatically; subscribe only when you resolve token values in
JS.

## The one carve-out: art whose color is its identity [#the-one-carve-out-art-whose-color-is-its-identity]

A cherry is red and a bell is gold in every theme. Recoloring recognizable
symbol art per theme just produces shapes nobody recognizes, so a symbol set
may carry its own palette: declared once as custom properties, used by
nothing but the art, with everything around it fully tokenized. The Slot
Machine's reel symbols are the worked example:

```css
:root {
  --slot-cherry: #e0374b;
  --slot-leaf: #5aa84f;
  /* ...the symbol palette, used only by the symbol art */
}
```

The cabinet, reels, payline and type around those symbols all stay on
`--dd-*` tokens and restyle with the theme. Keep the carve-out that narrow.

## Fonts [#fonts]

Never write a literal `font-family`. Text flows through the three face
tokens (`--dd-font-sans`, `--dd-font-display`, `--dd-font-mono`), which
themes may repoint. When a widget's look depends on a specific face, pin one
of the bundled fonts in the manifest instead:

```json
"fonts": { "use": { "display": "audiowide" } }
```

The stylesheet keeps using `var(--dd-font-display)`; the pin rewrites that
token in your documents only. One subtlety: the pin is invisible to
`dd.theme.tokens()` (the raw host map) but visible to `dd.ui.token()`
(computed style), so canvas widgets read faces with `dd.ui.token`. Bundling
your own font files is covered in
[the manifest reference](/docs/reference/manifest#fonts).

## Autoscale: readouts that follow the widget's size [#autoscale-readouts-that-follow-the-widgets-size]

Fixed type tokens are right for chrome, but a big readout should grow with
its widget. The opt-in `.dd-autoscale` class scales everything inside with
the container's smaller edge, tuned by two tokens:

```css
#root {
  /* gentler than the default 36cqmin / 44px */
  --dd-autoscale-size: 6cqmin;
  --dd-autoscale-max: 20px;
}
```

Rules that make it behave:

* The class must sit on an element whose size comes from outside (absolute
  inset, or percentage width and height). A content-sized box collapses
  under container sizing.
* Size everything inside in `em`, inline SVG included, so it all rides the
  scaled font.
* Opt a single element out with a plain `font-size`.

Because the knobs are tokens, a theme can retune autoscale globally and your
per-widget values still layer on top.

Both knobs also multiply by `--dd-text-scale`, the user's Text size when a
widget declares the [`textScale` preset](/docs/reference/manifest#setting-presets)
(app 0.3.4+), so an autoscale readout follows that setting with no code of
its own.

## Motion [#motion]

Transitions and animations time themselves with `--dd-anim-fast`,
`--dd-anim-slow`, and `--dd-ease`, so a theme can retime the whole desktop,
and the user's Animation speed setting scales the two durations on top of
the theme. Use them in your own transitions too. Two freebies and one rule:

* The OS reduced-motion preference disables the house motion automatically;
  you never guard for it.
* Keep custom keyframes to `opacity` and `transform`. Geometry-changing
  animations fight the input-capture tracker, which re-measures your
  [interactive regions](/docs/guides/interactivity) when layout changes.

## What themes do at runtime [#what-themes-do-at-runtime]

Knowing what actually changes helps you trust the tokens:

* **The automatic theme** derives the accent family and surface and border
  tints from the user's current wallpaper, and follows wallpaper changes
  live, slideshows and live wallpaper apps included. The user chooses how
  strong the tint is and whether it also sets the five chart colors, and can
  change the rest of the look under it: fonts, corners, spacing, text and
  status colors. You cannot assume any particular accent hue, ever.
* **Theme changes fade.** Colors move to the new theme over about 0.65 s
  (instant with Animation speed Off, reduced motion or low-spec mode), and
  `dd.theme.onChange` runs once they have settled.
* **The panel opacity slider** (Settings, Themes) retunes the alpha of
  `--dd-surface` and `--dd-surface-strong` under any theme.
  `--dd-surface-transparent` is never touched; it is transparent by
  contract.
* **Per-widget style** (the Style tab of a widget's settings window) lets
  a user override any token on one widget instance: background, border,
  corner radius, fonts, colors. Your CSS sees a different `var()` value and
  nothing else changes; the edits preview live through the same
  `theme.tokens` event as a theme switch.

The takeaway for both: build surfaces from the surface tokens (or just use
`dd-panel`, which does) instead of painting your own backgrounds, and all of
this behavior reaches your widget for free.

## The dev kit stylesheet [#the-dev-kit-stylesheet]

A scaffolded widget's CSS starts with
`@import "./.deskdash/dd-tokens.css";`, a generated file holding every token
at its default value. That import exists so the page renders sensibly in a
plain browser and so your editor can autocomplete token names. Inside
DeskDash it never fights the real theme: the SDK applies the live values as
inline style on the document root, which outranks any stylesheet.


# Working with data (/docs/guides/working-with-data)



## What the sandbox means for data [#what-the-sandbox-means-for-data]

A widget document has no network and no browser storage. `fetch`, `XHR` and
WebSockets are blocked by the sandbox's content security policy, remote
image URLs will not load, and the sandboxed frame has no `localStorage`,
cookies, or IndexedDB. This is not a missing feature, it is the security
model: everything a widget does to the outside world goes through the SDK,
where the manifest's grants apply.

So for data there are exactly two doors:

* **`dd.http.fetch`** for the network, against hosts declared in the
  manifest.
* **`dd.storage`** for persistent state, a small per-instance key-value
  store.

## Fetching over HTTP [#fetching-over-http]

Declare the exact hostnames the widget talks to, and nothing else works:

```json
"permissions": {
  "http": { "hosts": ["api.open-meteo.com", "geocoding-api.open-meteo.com"] }
}
```

Users see this list word for word at install time ("Talks to the internet,
only to: ..."), which is why exact hosts beat wildcards: a widget that
names two API hosts is easy to trust. Then:

```js
const res = await dd.http.fetch({
  url: "https://api.open-meteo.com/v1/forecast",
  query: { latitude: "44.43", longitude: "26.10", current: "temperature_2m" },
});
if (res.status === 200) render(res.json);
```

JSON responses arrive pre-parsed on `res.json`; text rides `res.bodyText`;
images can be fetched with `as: "dataUrl"` and fed straight to an `img` or
`dd-icon` (the sandbox allows `data:` images). Redirects, methods, header
rules, size caps and pacing are all specified in
[the http reference](/docs/reference/api/data-services#ddhttp).

## Polling with dd.poll [#polling-with-ddpoll]

Any fetch-on-a-timer goes through `dd.poll`. Never hand-roll the loop: the
helper owns the interval, exponential backoff on errors, and staleness, and
it keeps polling paced even when your render throws. The Weather widget is
the complete pattern in one place, settings included:

```js
let settings = {};

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

dd.poll({
  // the settings the fetch below reads, coalesced (see the note after)
  refreshOn: ["city", "units", "refreshMin"],
  // read each cycle, so the interval follows the live setting
  every: () => Math.min(180, Math.max(10, Number(settings.refreshMin) || 30)) * 60_000,
  fetch: async () => {
    const query = (settings.city || "").trim();
    if (!query) throw new Error("Set a city in settings");
    const geo = await dd.weather.geocode(query);
    const current = await dd.weather.current({
      lat: geo.lat, lon: geo.lon,
      units: settings.units || "celsius",
    });
    return { current, place: geo.label };
  },
  onData: ({ current, place }) => {
    const d = dd.weather.describe(current.code, current.isDay);
    temp.value = `${Math.round(current.temperature)}°`;
    condition.value = d.label;
  },
  onError: (err) => {
    temp.value = "--°";
    condition.value = `${err?.message || err} · retrying`;
  },
});
```

The pieces worth copying even when your data source differs: `every` as a
getter so the interval follows a live setting, `refreshOn` instead of a
`refresh()` call in the settings callback, a `fetch` that throws to signal
failure, and an `onError` that degrades the UI honestly instead of freezing
the last good value.

`refreshOn` is the one that bites if you skip it. The settings dialog
live-previews as the user types, one change per keystroke, so calling
`refresh()` from the settings callback re-fetches once per CHARACTER: typing
`Amsterdam` is nine geocodes, and `dd.http.fetch` allows 30 requests a minute.
Naming the keys instead waits for them to rest (400ms) and polls once. Keys
you leave out, a panel restyle for instance, never cost a request. Backoff after errors is automatic (from 1 second up to your
interval).

Pick intervals that match how fast the data actually changes. Weather
defaults to 30 minutes with a floor of 10; a stock ticker might justify a
minute. Every instance of your widget polls independently, and rate limits
apply per instance.

## Secrets in requests [#secrets-in-requests]

APIs that need a key are the reason the `secretRef` setting type exists.
Declare one, and the user picks a stored credential in the settings dialog;
your code then references it by setting key, and the host injects the real
value after validating the request:

```json
"settings": [
  { "key": "apiKey", "type": "secretRef", "label": "API key" }
]
```

```js
const res = await dd.http.fetch({
  url: "https://api.example.com/v1/me",
  headers: { authorization: { $secret: "apiKey" } },
});
```

The widget never sees the secret's value: not in JS, not in logs, not in
error messages. Never ask users to paste keys into a plain `text` setting;
that stores the key as an ordinary readable setting and defeats the whole
mechanism.

## The built-in weather stack [#the-built-in-weather-stack]

Weather is common enough that the SDK ships it: `dd.weather.geocode`,
`current`, `describe`, and `icon` wrap Open-Meteo with no API key needed.
It is convenience, not privilege: the calls route through your own grants,
so the manifest still declares the two Open-Meteo hosts (and `storage` if
you want the geocode cache). Shapes in
[the reference](/docs/reference/api/data-services#ddweather).

## Small persistent state [#small-persistent-state]

`dd.storage` is a per-instance key-value store for JSON-serializable
values: caches, counters, the state a widget should wake up with. The
Pomodoro widget persists its timer and, crucially, **validates on
restore**:

```js
const stored = await dd.storage.get("state");
if (stored && typeof stored === "object" && typeof stored.phase === "string") {
  state = {
    phase: ["work", "short", "long"].includes(stored.phase) ? stored.phase : "work",
    running: stored.running === true,
    endsAt: Number(stored.endsAt) || 0,
  };
}
```

Stored shapes outlive widget versions: what you wrote in 0.1 will be read
by 0.3, so never trust a stored value's shape blindly. Field-by-field
validation with defaults, as above, makes updates painless. Use
`dd.storage.delete(key)` to evict stale cache entries. The store is small
by design (caps in
[the reference](/docs/reference/api/settings-storage#ddstorage)); it is for
state, not datasets.

## Prefer events over polling [#prefer-events-over-polling]

Before you poll, check whether the host already pushes what you need.
System vitals, now-playing metadata, audio spectrum frames, time ticks,
desktop and folder changes, and widget-state changes all arrive as
[events](/docs/reference/events): fresher than any polite polling interval
and cheaper for everyone. The change events that carry no payload
(`desktop.changed`, `folders.changed`, `widgets.changed`) follow one
pattern: the ping tells you your listing is stale, the matching `list` call
is the read path.

Polling is for data the host does not have, which in practice means
external APIs.

## When the network is down [#when-the-network-is-down]

Widgets live on the desktop through sleep, wifi changes, and offline
stretches, so failure is a normal state, not an exception:

* Let `dd.poll`'s backoff do the retrying; do not add your own retry loop
  on top.
* Degrade visibly but calmly: keep the layout, blank the values, note the
  retry (the Weather widget shows the error and "retrying" in its
  condition line).
* Show staleness honestly. A [`.dd-status`](/docs/guides/components#shared-patterns-worth-knowing)
  line with "updated 3h ago" beats silently displaying old numbers as
  current.
* Cache the last good result in `dd.storage` if the widget is useful with
  slightly old data; validate it on restore like any stored shape.


# Design guidelines (/docs/native/guidelines)



A widget feels native when a user cannot tell where DeskDash ends and your
work begins: it follows their theme, it behaves like the widgets around it,
and it stays quiet until looked at. Most of that comes from the platform
for free, if you take what it offers. These are the habits that keep you
on that path; each links to the guide that shows the how.

## Components first [#components-first]

Build chrome from the [`dd-*` components](/docs/guides/components): the
panel, the text roles, the buttons, chips, and meters. They carry the house
look, react to themes, and auto-wire shared settings. Custom markup is for
what is genuinely yours (the visualization, the art, the layout); the
frame around it should be stock. And inside your own code, the second time
you build the same shape, unify it rather than copying it.

## Tokens, always [#tokens-always]

Every color, size, font, radius and duration comes from a
[`--dd-*` token](/docs/guides/theming). One literal hex value is all it
takes to break theming; a user who switches to a light theme, or lets the
automatic theme follow their wallpaper, will find your widget the one thing
on the desktop that did not come along. The single carve-out is symbol art
whose color is its identity (a cherry is red in every theme); keep that
palette private to the art and tokenize everything around it.

## Structure in HTML [#structure-in-html]

Someone opening your `index.html` should see what the widget is: its
regions, its parts, its shape. Keep the skeleton in markup, update values
in place, and build DOM in JavaScript only where structure genuinely
varies. It is a code-quality rule, but also a design one: widgets built
this way tend to have a clear, calm hierarchy, because the hierarchy had
to be written down.

## Motion that stays out of the way [#motion-that-stays-out-of-the-way]

The desktop is a background, and nothing on it should move uninvited.
DeskDash's own motion is opt-in and one-shot: entrance animations play
once, controls transition their hover and press states subtly, and all of
it respects the OS reduced-motion preference without you doing anything.
The user's Animation speed setting scales the motion tokens too, so motion
timed with them follows the user's choice for free.
Follow the same rules in your own motion: animate opacity and transform
only, time it with the motion tokens, and reserve continuous animation for
things that genuinely represent live data (a spectrum, a sweep second
hand), never for decoration.

## Respect the click-through desktop [#respect-the-click-through-desktop]

At rest, the desktop is the user's. Capture input only where your widget is
genuinely [interactive](/docs/guides/interactivity), mark exactly what
should feel solid, and let everything else pass through. A glanceable
widget often needs no capture at all: ghost hover gives you presence (a
highlight as the cursor passes) at zero cost to the desktop underneath.
Widgets that grab more than they use are the fastest way to make a desktop
feel broken.

## Icons [#icons]

Use the built-in glyphs for UI verbs (play, close, settings, refresh); they
tint with the text color and follow the theme. Ship your own SVG for
identity art, one file per icon, tinted when it should behave like text and
full-color when the color is the point. Stateful, animated, or multi-part
art is widget code, built with `dd.ui.create` and bound to your data, not a
pile of swapped image files.

## Density and restraint [#density-and-restraint]

A widget is furniture, not an app. It should answer its one question at a
glance and otherwise stay quiet:

* Prefer one clear readout over four small ones. If everything is
  emphasized, nothing is.
* Use the house empty state (`.dd-empty`) and status line (`.dd-status`)
  instead of inventing chrome for "nothing here" and "last updated".
* An idle state should look calm, not busy. Blank space on a desktop is a
  feature.
* Test the widget over a bright wallpaper, a dark one, and a photo. The
  surface tokens handle this; hardcoded backgrounds do not.

## A taskbar that feels native [#a-taskbar-that-feels-native]

A [taskbar](/docs/guides/taskbars) is a screen-edge surface, and people
use it without looking. Five habits keep it honest:

* The corner is the start button. People throw the cursor into the screen
  corner and click, so the start button's box must reach the edge and the
  corner whatever the visual padding; the bundled bars do it with a
  negative margin that the logo's own padding gives back.
* Buttons never jump. Render from the host's strip and never keep an order
  of your own: a focus change or a launch must not move a button.
* Previews open on a click or after a dwell, never on a sweep across the
  strip, and the host's state stream, not your click, decides what is open.
* No pin list and no app menu of your own. Pins belong to the app config
  and the actions are host privileges; tag your buttons and the host draws
  the menu.
* Motion rests while the user drags. When `<dd-apps>` carries `sorting`,
  hover waves and inline geometry pause; and the headroom band above the
  strip stays click-through, so a maximized window beneath keeps working.

## Settings that respect the theme [#settings-that-respect-the-theme]

Every setting is a question the user has to answer, so add them
deliberately. Use the shared presets
([panel style and accent color](/docs/guides/settings)) rather than
inventing your own variants of the same ideas, and do not add settings for
things the theme already decides; a widget with its own font picker or
corner-radius slider is fighting the system it lives in. The best settings
change what the widget shows, not how DeskDash looks.


# Performance and quality (/docs/native/performance)



A widget is not a page someone closes. It runs for days on a desktop that
is usually being ignored, next to a dozen others doing the same. The bar
is simple to state: no leaks, and no work whose result nobody is looking
at.

## Poll like a good neighbor [#poll-like-a-good-neighbor]

Every timer-driven fetch goes through
[`dd.poll`](/docs/guides/working-with-data#polling-with-ddpoll), and the
interval should match how fast the data actually changes: weather is a
30-minute question, a build status maybe a minute. Error backoff is
automatic; do not stack your own retries on top. Remember that every
instance polls independently and
[rate limits](/docs/reference/api/core#how-permission-gating-works) are per
instance, so an interval that seems fine once should still seem fine three
times.

## Events beat timers [#events-beat-timers]

Never rebuild a data source the host already pushes. The one-second clock
timer is the canonical mistake: `dd.time.onTick` exists, is second-aligned,
and costs nothing extra. The same goes for system vitals, now-playing
state, the audio spectrum, and folder changes; the
[event reference](/docs/reference/events) is the menu. A widget with no
timers of its own is usually a widget with no drift, no leaks, and no
wasted wakeups.

## Animation frames: when to run and when to stop [#animation-frames-when-to-run-and-when-to-stop]

A `requestAnimationFrame` loop is a promise to do work every frame, so it
must be tied to something visibly changing:

* Drive canvas drawing from the data stream where possible. The audio
  spectrum stream, for example, simply stops sending frames when playback
  goes silent; a visualizer that draws per received frame stops with it,
  free.
* If you do run your own loop, stop it when the data goes static, and
  render nothing at all when values did not change.
* State changes (hover, expand, show) animate with CSS transitions, not
  frame loops, using opacity and transform only.

## The low-spec flag [#the-low-spec-flag]

The `dd.ready` context carries `perf.lowSpec`, set when the user's machine
or settings ask widgets to go easy. Honor it in canvas-heavy widgets: cap
your frame work around 30 fps and render at 1x device pixels instead of
the display's scale factor. Some streams adapt on their own (the spectrum
drops its rate); your own drawing is your job.

## Effect and subscription hygiene [#effect-and-subscription-hygiene]

* Every `on*` subscription returns an unsubscribe function. If a
  subscription belongs to a region you rebuild, keep the function and call
  it when the region goes; the SDK's auto-dispose of bindings is a
  backstop, not a lifecycle.
* Keep [effects](/docs/guides/components#signals-and-effects) small: one
  element, one concern. An effect that writes a signal another effect
  reads is how update storms start; derive with `computed` instead.
* Follow [the one rule](/docs/guides/components#the-one-rule): bindings on
  built-once structure, no bindings inside rows a re-render discards.

## Never block the main thread [#never-block-the-main-thread]

The host pings widgets for liveness; a widget that stops answering (a
synchronous loop, a giant JSON parse) is declared crashed and replaced with
a reload overlay. Chunk heavy work, keep per-event handlers small, and
remember that a busy-looping widget also blocks its own reloads until the
loop ends. If you need to burn CPU, something upstream of your widget is
usually wrong.

## The extremes of the grid [#the-extremes-of-the-grid]

Users will make your widget both smaller and larger than you designed it:

* Set `minSize` to the smallest genuinely useful shape, then actually test
  it there. Truncate honestly (ellipsis, fewer rows) instead of clipping
  or overlapping.
* Test large too. Readouts should scale (the
  [autoscale pattern](/docs/guides/theming#autoscale-readouts-that-follow-the-widgets-size)),
  and lists should scroll rather than stretch.
* Sizes are grid cells whose pixel size varies per user, so test by
  resizing, not by trusting numbers.

## The pre-publish self-check [#the-pre-publish-self-check]

Fifteen minutes with your own widget before
[submitting](/docs/guides/publishing) catches most rejections and most
one-star moments:

1. Switch themes, including Automatic over a bright and a dark wallpaper.
   Nothing should stay stuck in your palette.
2. Resize to `minSize` and to something huge. Both should look deliberate.
3. Kill the network. The widget should degrade calmly, say so, and recover
   on its own when the network returns.
4. Let it run for hours. Memory flat, log quiet, no badge.
5. Open the log and watch a few minutes of normal operation; a widget that
   logs on every tick is noise nobody can debug around.
6. Re-read your manifest permissions and delete any the widget no longer
   uses; every extra grant is install friction.
7. Run two instances side by side. They should not fight over storage or
   look identical when configured differently.
8. Toggle reduced motion in Windows. Your custom animations should honor
   it like the house ones do.


# Component reference (/docs/reference/components)



The house style ships as Web Components: custom elements that come
pre-styled from the design tokens and react to theme changes for free.
Nothing to import; linking the SDK registers every `dd-*` element for the
document.

Each element renders into a Shadow DOM, styled by the same shared stylesheet
raw-class widgets use, so a theme change (which rewrites the `--dd-*` tokens
on `:root`) restyles component internals automatically. Your own CSS cannot
reach inside with ordinary selectors; use the exposed hooks instead:
`::part(name)`, the documented custom-property knobs, and named slots.

Unless a component says otherwise, it is **presentational**: clicks bubble
from the host element and you wire them yourself; nothing acts on its own.

## dd-text [#dd-text]

Typography. Content is projected; the host carries the font and color.

```html
<dd-text variant="title">Tasks</dd-text>
<dd-text variant="muted">3 open</dd-text>
<dd-text size="xl" weight="bold">42</dd-text>
```

`variant` picks a semantic default (size, weight, color):

| variant          | Use                                                                   |
| ---------------- | --------------------------------------------------------------------- |
| `body` (default) | plain body text                                                       |
| `title`          | panel or section heading (semibold, large, display face)              |
| `subtitle`       | secondary heading (muted, medium)                                     |
| `label`          | uppercase tracked label                                               |
| `muted`          | secondary or status text                                              |
| `value`          | big readout (semibold, tabular, display face; add `mono` for metrics) |

`size` (`xs` to `2xl`), `weight` (`normal`, `medium`, `semibold`, `bold`)
and the boolean `mono` override or extend the variant; they map straight
onto the [type scale tokens](/docs/reference/tokens#type-scale). The
attribute values are the complete vocabulary: anything else does nothing.
For a truly special size, put a plain `font-size` on the element; an id or
class selector beats the variant default.

## dd-icon [#dd-icon]

```html
<dd-icon name="plus"></dd-icon>               <!-- built-in glyph, tinted -->
<dd-icon src="icons/rain.svg"></dd-icon>      <!-- your SVG, full color -->
<dd-icon src="icons/rain.svg" tint></dd-icon> <!-- your SVG, masked to the text color -->
```

* `name` picks a built-in glyph, drawn as a currentColor mask so it follows
  the text color and themes. The full set: `battery`, `check`,
  `chevron-down`, `chevron-up`, `close`, `gear`, `lock`, `pause`, `play`, `plus`, `power`,
  `refresh`, `search`, `sign-out`, `skip-back`, `skip-forward`, `sleep`,
  `volume`, `volume-mute`, `wifi`. Names are validated (lowercase, digits,
  hyphens); anything else is ignored.
* `src` renders an SVG file from your widget folder (or a `data:` URI) as a
  real image, so the SVG's own colors show. Add `tint` to mask it in the
  current text color instead. The image carries part `img`.

Size via `--dd-icon-size` (default `1.25em`). Swap at runtime with
`icon.setAttribute("src", ...)` or bind it:
`create("dd-icon", { src: () => ... })`.

There is no external-sprite mode (one file, many icons by name); ship one
SVG per icon, or build stateful art inline with `dd.ui.create`.

## dd-panel [#dd-panel]

The bordered card that fills the widget and stacks its children.

```html
<dd-panel heading="Weather" settings="panelStyle">
  <button slot="actions" class="dd-icon-btn">...</button>
  <div class="dd-scroll">...</div>
</dd-panel>
```

To make the body scroll, wrap it in `<div class="dd-scroll">` as above: the
class sets `overflow-y: auto` and lets the div shrink inside the panel. Every
scrolling element gets the DeskDash scrollbar from app 0.4.3, inside the
components too. On older apps only a `.dd-scroll` element has it, so scroll
the wrapper rather than the panel's `surface` part.

| Attribute               | Effect                                                                                    |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| `heading="..."`         | render a title row. Use `heading`, not `title` (the native tooltip)                       |
| `transparent`           | glassy surface instead of solid                                                           |
| `bare`                  | no paint at all: the card keeps its box (padding, layout) but drops background and border |
| `row`                   | lay children horizontally                                                                 |
| `settings="panelStyle"` | auto-wire the panel-style preset (see below)                                              |

| Slot      | For                                                   |
| --------- | ----------------------------------------------------- |
| (default) | the panel body                                        |
| `heading` | a custom or dynamic heading, instead of the attribute |
| `actions` | controls pinned to the right of the header            |

| Part      | For                                                |
| --------- | -------------------------------------------------- |
| `surface` | the card itself (override padding, layout, border) |
| `header`  | the heading row                                    |
| `heading` | the heading text                                   |

Knob: `--dd-panel-pad` sets the surface padding (default
`var(--dd-space-3) var(--dd-space-4)`). The header row renders only when
there is a `heading` attribute or slotted `heading`/`actions` content; an
empty header hides itself.

```css
/* the surface lives in the shadow, so reach it via the part */
#panel::part(surface) { flex-direction: row; gap: 4vw; padding: 0; }
```

## dd-bar [#dd-bar]

A bar segment, for `kind: "bar"` (taskbar) widgets only. Wrap the bar's
visible content in one or more of these: each segment's box is the area
that takes the mouse, and bar-window space outside every segment clicks
through to the desktop or the maximized window beneath. Items may animate
between segments; the SDK follows the boxes automatically.

```html
<dd-bar place="center" reach="35">
  <dd-panel row id="dock">...</dd-panel>
</dd-bar>
```

| Attribute    | Effect                                                                                                                                                                                                                |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `edge`       | `left` or `right` (absent = bottom). The screen edge the bar window docks to; set it from `ctx.bar.edge`. The host fixes to that edge and lays out along it, as a column on a side edge.                              |
| `place`      | where along the edge. Bottom: `left`, `center`, `right` or `fill` (default). Side edges: `top`, `center`, `bottom` or `fill`. The first three shrink-wrap the content, `fill` spans the edge.                         |
| `reach="35"` | extra interactive headroom on the inner side of the content (above a bottom bar, beside a side bar), in CSS px, inside the capture box. Hover and magnify animations that rise past the surface stay hoverable in it. |
| `pill`       | paint the surface with the house pill look (rounded-full, strong surface, border, shadow). Default is unpainted: the segment is first a capture container; put a `dd-panel` (or anything) inside for visuals.         |

Part: `surface` (the inner edge-aligned flex row, a column on a side edge). The element marks
itself `data-dd-interactive` one settled frame after connect; that first
report is what switches the bar window from native input everywhere to
segment-scoped capture. See [Interactivity](/docs/guides/interactivity) for
the bar subsection. Grid widgets have no use for this element.

## dd-button [#dd-button]

The standard button. `variant` is `solid` (default), `ghost`, or `outline`;
content (a label, a `dd-icon`) is projected; `disabled` blocks it. Part:
`button`.

```html
<dd-button>Save</dd-button>
<dd-button variant="ghost">Clear done</dd-button>
<dd-button variant="outline">Connect</dd-button>
```

```js
btn.addEventListener("click", () => { /* ... */ });  // host click bubbles out
btn.setAttribute("disabled", "");                    // reflects onto the inner button
```

## dd-icon-button [#dd-icon-button]

The square (or `round` circle) icon button: transparent at rest, surface on
hover. The glyph comes from `name` (a built-in glyph) or the slot (a
`dd-icon`, or custom SVG built with `dd.ui.create`). Boolean attributes:
`round`, `active` (accent border and text), `off` (muted), `disabled`.
Part: `button`.

```html
<dd-icon-button name="plus" title="Add"></dd-icon-button>
<dd-icon-button round name="play" title="Play"></dd-icon-button>
<dd-icon-button round><dd-icon name="chevron-down"></dd-icon></dd-icon-button>
```

```js
/* stateful toggle: swap the built-in glyph by name */
playBtn.setAttribute("name", playing ? "pause" : "play");
```

## dd-meter [#dd-meter]

The horizontal track-and-fill meter. `value` is 0..100 (clamped).
`severity` (`success`, `warning`, `danger`) swaps the fill to the matching
status token; without it the fill is `--dd-meter-color` (default: the
accent), so a widget can inject a resolved custom color. Height via
`--dd-meter-height` (default `0.4em`, so it rides the surrounding font
size). Parts: `track`, `fill`.

```html
<dd-meter value="63"></dd-meter>
<dd-meter value="97" severity="danger"></dd-meter>
```

```js
meter.setAttribute("value", String(pct));
meter.style.setProperty("--dd-meter-color", resolvedAccent);
```

## dd-chip [#dd-chip]

A pill toggle. Boolean attributes: `dim` (low emphasis), `active` (accent
border and text), `disabled`. Content is projected. Part: `chip`.

```html
<dd-chip dim>Other networks...</dd-chip>
<dd-chip active>On</dd-chip>
```

```css
/* an app-specific state, via the part */
dd-chip.is-hidden::part(chip) { text-decoration: line-through; }
```

## dd-app-tile [#dd-app-tile]

A launcher tile: an icon (or a letter fallback) with an optional label.

```html
<dd-app-tile src="icons/steam.png" label="Steam"></dd-app-tile>
<dd-app-tile glyph="S" label="Steam"></dd-app-tile>
<dd-app-tile add label="Add"></dd-app-tile>
```

| Attribute           | Effect                                                 |
| ------------------- | ------------------------------------------------------ |
| `src="..."`         | an image icon (full color)                             |
| `glyph="A"`         | a letter or emoji fallback when there is no image      |
| `add`               | the dashed plus affordance (ignores src and glyph)     |
| `label="..."`       | caption under (or beside) the icon; omit for icon-only |
| `orientation="row"` | lay icon and label horizontally                        |
| `disabled`          | block the click                                        |

Parts: `tile` (the button surface), `icon` (the icon box), `label`. Size the
host from your own CSS; the icon box follows `--dd-tile-icon` (default
`2em`).

```js
tile.addEventListener("click", () => dd.apps.launch(app.id));
dd.peers.draggable(tile, { /* ... */ });   // binds to the host fine
```

```css
dd-app-tile { width: 56px; height: 56px; --dd-tile-icon: 32px; }
dd-app-tile::part(label) { color: var(--dd-text-muted); }
```

## dd-volume-pill [#dd-volume-pill]

Mute button, slider, percent readout, and a sound-settings shortcut in one
element. The one behavior-ful component: it subscribes to the vitals stream
and drives the system volume itself, so the manifest must grant `system`
and `systemControls`. Failed calls log a warning and the pill stays up.
Built for popout pane documents, but works anywhere.

* `input` (app 0.4.2 or newer) drives the default microphone instead of the
  speakers, with a microphone glyph on the mute button. The pill hides on a
  machine with no microphone, and on an older app (which also logs one
  warning).
* `no-settings` hides the settings gear.
* `devices` (app 0.4.2 or newer) adds a chevron between the percent and the
  gear. It drops a card below the pill with a
  [`<dd-audio-devices>`](#dd-audio-devices) in it: outputs and inputs, or
  only the inputs on an `input` pill; the default checked, a click
  switches. The card is an overlay, so
  the layout under the pill does not move, but your document needs room for
  it (about 12em below the pill; it scrolls past 14em). A second chevron
  click or a press outside closes it. Leave the attribute off in a pane only
  as tall as the pill.
* Parts: `pill`, `slider`, `value`, `devices` (the card). The mute, chevron
  and gear buttons expose no parts.
* Slider input is throttled internally to roughly 10 volume calls per
  second, and the thumb and percent hold still during a drag.
* Event `dragchange` (bubbling, composed, `detail: { dragging }`) fires as
  the user grabs and releases the slider. A pane document should relay it so
  the base widget's [flyout helper](/docs/reference/api/widgets-together#ddpopoutflyout)
  holds its hover-retract during a drag:

```js
pill.addEventListener("dragchange", (e) => dd.popout.send(e.detail));
```

## dd-audio-devices [#dd-audio-devices]

The audio device list, with a check on the Windows default. A click makes
that device the default for its direction (output or input) in every role,
as the Windows sound menu does.

```html
<dd-audio-devices flow="output"></dd-audio-devices>
```

Behavior-ful like the volume pill, so the manifest must grant `system` and
`systemControls` (app 0.4.2 or newer). Without the API or the grants it
logs one warning and hides itself, so on an older app it simply shows
nothing.

* `flow`: `output` (default), `input`, or `both` (two groups with Output
  and Input headings).
* It lists on connect, lists again whenever a device comes, goes or becomes
  the default, and paints the list a switch answers, never its own guess.
* Event `dd-pick` (bubbling, composed, `detail: { id }`) on every row click.
* Parts: `list`, `heading`, `row` (the default carries
  `aria-pressed="true"`), `check`, `name`, `empty`.

## dd-audio-mixer [#dd-audio-mixer]

The per-app volume mixer: one row per app with sound on the default output
device (icon, name, slider, percent, mute), as the Windows volume mixer
shows them, playing apps first and "System sounds" last.

```html
<dd-audio-mixer></dd-audio-mixer>
```

Behavior-ful: the manifest must grant `system` and `systemControls` (app
0.4.2 or newer). On an older app it logs one warning and hides.

* Windows sends no event for app sessions, so the mixer polls
  [`dd.system.audioApps`](/docs/reference/api/system#ddsystemaudioapps)
  every 1.5 seconds while it is in the document, and stops when removed.
  Rows are kept by id, so a poll never moves a slider under the user's
  thumb.
* `no-system` drops the "System sounds" row; `no-settings` hides the link
  to the Windows volume mixer page.
* Slider input is throttled to about 10 calls a second per row.
* Event `dragchange` (bubbling, composed, `detail: { dragging }`), the same
  shape as the volume pill's: relay it from a pane document the same way.
* Parts: `list`, `row`, `icon`, `name`, `slider`, `value`, `mute`, `empty`,
  `settings`.

## dd-radio-toggle [#dd-radio-toggle]

A Quick Settings tile for one radio: a glyph, the name, and the state (the
Wi-Fi tile names the connected network). A click turns the radio on or off
through [`dd.system.setRadio`](/docs/reference/api/system#ddsystemsetradio).

```html
<dd-radio-toggle kind="wifi"></dd-radio-toggle>
<dd-radio-toggle kind="bluetooth"></dd-radio-toggle>
```

Behavior-ful: the manifest must grant `system` and `systemControls` (app
0.4.2 or newer). The tile hides itself on a machine with no such radio, and
on an older app (which also logs one warning).

* `kind`: `wifi` (default), `bluetooth` or `cellular`.
* The click is the gesture the host asks for. When Windows refuses (its
  "Let apps control device radios" privacy switch is off) the tile reads
  "Blocked in Windows settings" and the next click opens that page.
* Reflected host attributes for your CSS: `on`, `unavailable` (off by
  hardware switch or airplane mode; the tile is disabled), `pending` (a
  switch is on its way, up to 4 seconds), `denied`.
* Event `dd-toggle` (cancelable, bubbling, composed, `detail: { kind, on }`)
  before each switch; `preventDefault()` keeps the radio as it is.
* Parts: `tile` (carries `aria-pressed`), `glyph`, `label`, `state`.
* The tile fills its width; lay several out in a grid.

## dd-bluetooth-devices [#dd-bluetooth-devices]

The paired Bluetooth devices, connected first, each with a glyph for its
kind, its name and its state. Audio devices get a Connect or Disconnect
button, as in Windows Quick Settings.

```html
<dd-bluetooth-devices></dd-bluetooth-devices>
```

Behavior-ful: the manifest must grant `system` and `systemControls` (app
0.4.2 or newer). On an older app it logs one warning and hides.

* It polls
  [`dd.system.bluetoothDevices`](/docs/reference/api/system#ddsystembluetoothdevices)
  every 4 seconds while it is in the document and the radio is on. With the
  radio off it reads "Bluetooth is off" and stops polling. Pair it with a
  [`<dd-radio-toggle kind="bluetooth">`](#dd-radio-toggle).
* A Connect or Disconnect click is the gesture the host asks for. The row
  reads "Connecting..." until Windows reports the new state, or 8 seconds
  pass.
* `no-add` hides the "Add device" link, which opens the Windows Bluetooth
  page (there is no pairing here).
* Parts: `list`, `row`, `glyph`, `name`, `state`, `action`, `empty`, `add`.

## dd-brightness [#dd-brightness]

Brightness sliders: one per display whose brightness can be set (a laptop's
built-in panel, external monitors that answer DDC/CI), primary first. The
display names show only when there is more than one.

```html
<dd-brightness here></dd-brightness>
```

Behavior-ful: the manifest must grant `system` and `systemControls` (app
0.4.2 or newer). On an older app it logs one warning and hides.

* It polls [`dd.system.brightness`](/docs/reference/api/system#ddsystembrightness)
  every 5 seconds while it is in the document, and hides itself, with no
  warning, while the list is empty. That is normal on a desktop whose
  monitors have no DDC/CI, so keep its space in your layout optional.
* `here` shows only the display that holds this document, so a bar on each
  screen controls its own screen.
* `no-settings` hides the "Display settings" link.
* Slider input is throttled to about 7 calls a second, and the host keeps
  only the newest value per display.
* Event `dragchange` (bubbling, composed, `detail: { dragging }`), the same
  shape as the volume pill's.
* Parts: `list`, `row`, `glyph`, `name`, `slider`, `value`, `settings`.

## dd-apps and dd-app [#dd-apps-and-dd-app]

The taskbar's buttons, in one tag:

```html
<dd-apps settings="pins" indicator="dots"></dd-apps>
```

`<dd-apps>` renders one `<dd-app>` per entry of
[dd.bar.store()](/docs/reference/api/bar-widgets#ddbarstore) (the host's
[strip](/docs/reference/api/bar-widgets#ddbarstrip): pins and running apps
in the user's order, windows merged by `appKey`) as its own light-DOM
children, keyed and reconciled in place, never rebuilt. Every behavior is
the store's, so two bars composed from this click, launch, preview, pin
and reorder the same way: a launcher launches once, behind a lock that
holds until its window lands; a single window focuses, or minimizes when it
is the focused one; a stack toggles the host-drawn preview flyout; icons
are cached, the flyout state mirrors the host, the pins toggle gates the
launchers, a drag moves a row. Needs a `kind: "bar"` widget with the
`taskbar` permission.

Go only as deep as the look needs:

0. The tag above, paired with the `pins` preset: no widget code.
1. Restyle. The host is the box: size `dd-app` in widget CSS, measure it,
   write inline custom properties on it, animate its layout on it. State
   reflects as host attributes (`running`, `pin`, `focused`, `minimized`,
   `count="n"`, `noicon`, `launching`, `entering`, `leaving`, `dragging`,
   `low-motion`; `sorting` on `<dd-apps>` itself for a drag's duration),
   so `dd-app[focused] { outline: ... }` is a focus ring with no indicator
   at all. The anatomy is parts: `button` > `lift` >
   (`icon` | `fallback`) + `ring`; `indicator` > `dot` (one per window,
   capped by `max`); `label`. A host state goes in front of the part:
   `dd-app[focused]::part(dot)`, `dd-app:hover::part(label)`,
   `dd-app[launching]::part(lift)`.
2. Replace a piece. A `<template>` child of `<dd-apps>` is cloned into
   every entry as slotted light DOM (`slot="indicator"`, `slot="label"`
   replace the built-in parts) and widget CSS styles it as
   `dd-app[focused] > [slot=indicator]`. Cancelable events (bubble,
   composed): `dd-activate` `{ entry }` on click, `dd-launch` `{ entry }`
   before the launch, `dd-flyout-open` `{ entry, style }` before a preview
   opens (`detail.style` is the style the open sends; edit it in place),
   `dd-sort` `{ key, before }` on the drop of a sort, before the move
   (`before` is the entry key the row lands in front of, `null` for the
   end). `preventDefault()` replaces the default (for `dd-sort`: the host
   order comes back).
3. Your own reconcile: `<dd-app>` standalone, fed a `BarEntry` through its
   `entry` property, keeps its click, icon, menu tag and launch state.
4. Your own markup: `dd.bar.store()` and `dd.ui.create`.

* `<dd-apps>` attributes: `settings="pins"`; `indicator` = `dots` (default)
  \| `ticks` (stacked, for a side bar) | `none`; `max` (default 3); `label`
  \= `hover` (default) | `none`; `previews` = `click` (default) | `hover`
  (a 400 ms dwell opens the preview, a quick sweep opens nothing, a switch
  while one is open waits the same dwell, leaving a button closes
  nothing); `dwell` (ms); `stagger` (boot cascade step, ms, default 30);
  `sortable` (app 0.3.5+; drag to reorder: a press that travels past 6 px picks the
  entry up, it moves live under the pointer along the strip's axis, the
  release inside the segment commits the move through the store and the
  host's stream confirms it; a release outside the segment, a cancel or an
  unchanged order puts the host order back; the flyout closes and hover
  pauses for the drag, the click the release lands is swallowed, and every
  live move fires `dd-change`). `indicator`, `max` and `label` forward to
  every entry. A widget that writes inline geometry on the entries should
  rest while `<dd-apps>` carries `sorting`.
* Lifecycle: a new entry carries `entering` until its finite animations
  end (at once when none run); a removed one carries `leaving`, stops
  taking input and leaves the DOM when its animations end (700 ms
  backstop). Low motion skips both. The first render sets
  `--dd-app-delay` (and an inline `animation-delay`) per entry; read it in
  your own keyframes as `animation-delay: var(--dd-app-delay, 0s)`.
* Reorders slide: when entries land in new slots (a host emit, a drag
  shuffle), every surviving entry plays a short FLIP on the host transform
  (Web Animations, `--dd-anim-fast` / `--dd-ease`); low motion skips it.
  Leave the host `transform` alone in widget CSS and keep hover and press
  transforms on the parts, or the slide fights them.
* Geometry keyframes (width, height) belong on the host, in light DOM,
  where the capture-region tracker sees their `animationend`; parts
  animate transform and opacity only. Keyframes declared in widget CSS and
  applied through `::part()` resolve in the widget stylesheet's scope.
* `dd-change` (bubbles, `detail: { entries }`) fires on `<dd-apps>` after
  every settled reconcile, entrance and exit, for widget code that measures
  the entries. `apps.items` is the live entries in row order,
  `apps.appFor(key)` one of them.
* `<dd-app>` stamps the app-menu tag (`data-dd-app` / `data-dd-window` /
  `data-dd-pin`) itself, so a right-click opens the host's per-app menu;
  `disabled` (set by `dd.ui.pressGuard`) mirrors onto the inner button.
  `low-motion` on the host stops every animation and transition inside the
  shadow; outside it, override the tokens the components read
  (`body.low-motion { --dd-anim-slow: var(--dd-anim-fast) }`) rather than a
  `*` selector, which stops at the shadow boundary.

## dd-bar-button [#dd-bar-button]

The start button, a button for one of Windows' own menus, or the trigger
for a pane of the bar's own:

```html
<dd-bar-button action="start" label="Start"></dd-bar-button>
<dd-bar-button action="quick-settings" label="Quick settings"></dd-bar-button>
<dd-bar-button action="notifications" label="Notifications"></dd-bar-button>
<dd-bar-button action="pane" pane-w="560" pane-h="440" label="Games" icon="play"></dd-bar-button>
```

* `action="start"` taps the Windows start menu
  ([dd.bar.openStartMenu](/docs/reference/api/bar-widgets#ddbaropenstartmenu))
  behind the press guard. `action="quick-settings"` and
  `action="notifications"` open Windows' own Quick Settings and
  notification center
  ([dd.bar.openQuickSettings](/docs/reference/api/bar-widgets#ddbaropenquicksettings),
  [dd.bar.openNotifications](/docs/reference/api/bar-widgets#ddbaropennotifications),
  app 0.4.2 or newer), also behind the press guard. Windows places those
  menus in the bottom right corner, and the button never reflects `pressed`
  for them, because the host cannot see the menu close. `action="pane"`
  toggles the bar's own pane (the
  manifest `popout`, `pane-w` x `pane-h` CSS px, anchored on the button) and
  reflects `pressed` from the host's state only: the host closes the pane
  on its own (an outside press, Escape, a duck), so the pressed look follows
  `popout.changed`, never the click. No `action`: a plain button, the host
  click is yours. The `paneData` property reaches the pane as
  `ctx.popout.data`. Several pane buttons can share the bar's one pane, each
  with its own `paneData` (app 0.4.2 or newer): only the button that opened
  the pane looks pressed, and a click on another one switches the pane to
  its view instead of closing it.
* The icon is the default slot (an inline SVG whose parts you animate, a
  `<dd-icon>`) or the `icon` attribute (a built-in glyph name, or an SVG
  path rendered tinted). A start button with neither draws the four-pane
  logo itself, in `currentColor` and sized by `--dd-icon-size` on the host:
  parts `logo` and `pane`, `pane-1` to `pane-4`, so a bar can animate the
  panes one by one (`dd-bar-button:hover::part(pane-2) { animation-delay:
  150ms }`). A `quick-settings` or `notifications` button with neither
  draws a sliders or bell glyph (part `glyph`, stroked in `currentColor`,
  the same size). `label` is the hover label; `delay` (ms) staggers the
  entrance like an entry's.
* `badge` (app 0.4.2 or newer) shows a count or short text as an accent
  pill on the icon's top right corner (part `badge`), inside the lift so it
  moves with the icon. Absent or empty hides it. The text shows as given,
  so cap it yourself (`"99+"`).
* Parts: `button` > `press` (the pressed backplate, a real element because
  a pseudo-element on a shadow host never renders) + `lift` > the icon and
  `badge`; `label`. Reflects `pressed`, `entering`, `low-motion`, `disabled`. The
  host is the box, like `<dd-app>`.

## dd-tray-caret [#dd-tray-caret]

The system-tray caret for a bar widget, in one tag:

```html
<dd-tray-caret settings="systray"></dd-tray-caret>
```

It subscribes to the host's notification-area mirror
([dd.systray](/docs/reference/api/bar-widgets#ddsystray)) and opens or closes
the host-drawn list. Needs a `kind: "bar"` widget with the `taskbar`
permission; it warns and stays hidden otherwise. Paired with the `systray` preset there is no widget code at all.

* Hides itself until the setting is on and the host reports at least one
  icon, so a bar that does not use it costs no pixels.
* Reflects a `showing` attribute while visible, for neighboring layout in
  widget CSS.
* `separator` renders a leading hairline that hides with the caret.
* Parts: `button`, `separator`. Put the widget's box tuning on the host
  element; the shape stays the component's.
* The host closes the list on its own (an outside press, Escape, a left
  click on a row; a right-click keeps it open under the app's own menu),
  and the caret's pressed look follows that, never a local guess.

## dd-notification-bell [#dd-notification-bell]

The Windows notification center for a taskbar, in one tag:

```html
<dd-notification-bell label="Notifications" settings="notificationCenter"></dd-notification-bell>
```

A click opens the host's notification center beside the bell: other apps'
notifications, grouped by app, with their pictures and buttons. A click on
one there opens the app or jumps to the message, and the close and clear
buttons remove notifications from Windows too. Your widget never sees a
notification: the bell gets the count, the open state and the Do not
disturb mode through [dd.notificationCenter](/docs/reference/api/bar-widgets#ddnotificationcenter).
Needs a bar-kind widget with the `taskbar` permission, app 0.4.2 or newer;
on an older app it logs one warning and hides.

* It is a [`<dd-bar-button>`](#dd-bar-button) with that one job: the same
  parts (`button`, `press`, `lift`, `glyph`, `label`, `badge`) and
  attributes (`icon`, `label`, `delay`, `disabled`), so it styles like your
  other bar buttons. Style it by its own tag name
  (`dd-notification-bell::part(lift)`) next to your `dd-bar-button` rules.
* It draws a bell, or a moon while Do not disturb is on, and shows the
  count as its badge ("99+" above 99, none at 0).
* Reflects `pressed` while its window is open and `dnd` while Do not
  disturb is on. The host closes the window on its own (an outside press,
  Escape, a click that opened an app, the bar ducking or going away), and
  the pressed look follows that, never a local guess.
* `settings="notificationCenter"` follows the preset's "Show notifications"
  switch: hidden, and its window closed, while it is off. Without the
  preset the bell is always on.
* `<dd-bar-button action="notifications">` still opens Windows' own
  notification center.

## Settings presets on components [#settings-presets-on-components]

The `settings` attribute wires a component to a
[setting preset](/docs/reference/manifest#setting-presets) by name, and
accepts a comma-separated list. `<dd-panel settings="panelStyle">` observes
the `panel` setting and toggles its own `transparent`/`bare` attributes,
and (app 0.2.4+) paints the preset's optional background image by setting
`--dd-panel-image` / `--dd-panel-image-fit` on its surface, with zero widget
code. Raw-class widgets can set the same two custom properties on their
`.dd-panel` element by hand. Reading settings needs no permission, and
presets never write settings.

Two notes: the wiring happens once, when the element first connects
(changing the attribute later does nothing), and `accentColor` is a
fields-only preset with no component-side behavior; read it with
[dd.theme.accent()](/docs/reference/api/environment#ddthemeaccent) instead
of putting it in a `settings` attribute.

## Overriding [#overriding]

Two tiers, matching how much you want to change:

* **Style hooks.** `::part()`, the custom-property knobs, and named slots,
  as documented per component above.
* **Subclass and call super.** The constructors are exposed on
  `dd.ui.components` (`DdElement`, `DdPanel`, `DdText`, `DdIcon`,
  `DdButton`, `DdIconButton`, `DdChip`, `DdMeter`, `DdAppTile`,
  `DdVolumePill`, `DdTrayCaret`, `DdApps`, `DdApp`, `DdBarButton`,
  `DdNotificationBell`). Extend one, override a method (`build`, `render`,
  `applyPreset`), call the super method to keep the original behavior or
  skip it to replace it, then register your own tag:

```js
class MyPanel extends dd.ui.components.DdPanel {
  render() {
    super.render();                                   // keep the panel's own logic...
    this.surface.classList.toggle("my-flag", flag);   // ...and add to it
  }
}
customElements.define("my-panel", MyPanel);
```

## Building the rest [#building-the-rest]

Components cover chrome. Build everything else with
[dd.ui.create](/docs/reference/api/ui#dduicreate); components compose with
it and with each other (a `dd-panel` heading is itself a `dd-text`).


# Errors and versioning (/docs/reference/errors-versioning)



## Error codes [#error-codes]

Every failed bridge call rejects with an `Error` carrying a `code`:

| Code                | When                                                                                                                   |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `PERMISSION_DENIED` | the method or resource is not granted by the widget's manifest (or, for `dd.widgets`, the target is not user-approved) |
| `INVALID_PARAMS`    | params failed host-side validation                                                                                     |
| `NOT_FOUND`         | the referenced entity (a key, an item id, a saved network) does not exist; for ids, re-list and retry                  |
| `PAYLOAD_TOO_LARGE` | a request or response exceeds its documented cap                                                                       |
| `TIMEOUT`           | the operation timed out (host-side, or the SDK's own 15 second client timer)                                           |
| `RATE_LIMITED`      | a per-instance rate limit was exceeded; back off                                                                       |
| `GONE`              | the method existed in an older API and was removed. Permanent; the message names the replacement                       |
| `INTERNAL`          | an unexpected host error; details go to the host log                                                                   |

Handle errors like any other rejected promise, and degrade instead of
crashing:

```js
try {
  await dd.media.playPause();
} catch (err) {
  if (err.code === "RATE_LIMITED") return;  // ignore, the user is mashing
  dd.log("warn", "playPause failed", err.code);
}
```

Timeouts: the SDK rejects requests client-side after 15 seconds, except the
`dd.dialogs.*` pickers, which wait on the user (the host guarantees they
settle: a 1 hour backstop plus force-cancel on edit mode, reload, and
removal).

## apiVersion: the bridge contract [#apiversion-the-bridge-contract]

Your manifest's `apiVersion` declares which bridge API the widget was
written against. The host runs everything in the range
`[minSupported ..= version]`; both numbers ride `context.api` and are
currently both `1`.

* **Newer than the host**: the manifest is quarantined at scan ("requires a
  newer dashboard") and the widget never loads.
* **Older than `minSupported`**: the widget stays listed, but its instances
  render a "needs an update" tile instead of mounting, and every bridge call
  would fail `GONE`.

## Deprecation, then removal [#deprecation-then-removal]

API surface is never removed silently:

1. A method (or a change in its output) is first **deprecated**. It keeps
   working for at least one release; the SDK warns once per method in your
   own console (`[dd] <method> is deprecated: <note>`), and the current
   table rides `context.api.deprecated`.
2. When `apiVersion` bumps, deprecated methods move to **removed**: calls
   reject with `GONE` and a message naming the replacement.

Rules of thumb: watch your console during development, treat any `[dd]`
deprecation warning as a to-do before the next API bump, and feature-detect
with `context.api.version` when one widget build should span versions.

## minAppVersion: the feature floor [#minappversion-the-feature-floor]

`minAppVersion` and `apiVersion` gate different things. `apiVersion` is the
bridge contract. `minAppVersion` (a plain `x.y.z`) says "this widget needs
DeskDash x.y.z or newer":

* An app older than the floor quarantines the manifest with "needs DeskDash
  x.y.z or newer" instead of a raw parse error.
* The marketplace shows such a listing with a "Needs app update" chip and a
  disabled Install button.

**The store sets it for you.** The app keeps a table of the release that last
changed each part of the widget surface: every permission, setting type,
setting key, preset, bundled font and manifest field, the bar kind, and one
entry for everything no permission gates (ungated methods, the `dd.*`
helpers, the `dd-*` components, tokens and events). Every new version you
publish is stamped with the newest entry its manifest matches. A value you
write yourself only counts when it is higher, and a published version keeps
the floor it shipped with. Nothing to declare, and nothing to get wrong.

The releases behind the entries, for reference:

| Feature                                                                                                        | Since    |
| -------------------------------------------------------------------------------------------------------------- | -------- |
| the `power` permission                                                                                         | `0.1.9`  |
| the `folders` permission                                                                                       | `0.1.10` |
| the `games` permission                                                                                         | `0.3.2`  |
| the `launchers` and `feeds` permissions, the `url` setting type and a fractional `minSize`                     | `0.3.9`  |
| `kind: "bar"`, the `taskbar` permission and the `pins` / `systray` presets                                     | `0.3.0`  |
| `dd.bar.strip`, `bar.strip.changed` and `<dd-apps sortable>`                                                   | `0.3.5`  |
| `restartAdvanced` for `dd.power.run`                                                                           | `0.3.7`  |
| `dd.settings.open`, `dd.links.open`, `dd.dialogs.pickDesktopItems` and `dd.power.run` from a bar or its pane   | `0.3.7`  |
| `dd.feeds.open`                                                                                                | `0.3.11` |
| `dd.notificationCenter`, `<dd-notification-bell>`, the `notificationCenter` preset and `<dd-bar-button badge>` | `0.4.2`  |
| `placeholder` and `pick` on setting fields                                                                     | `0.1.10` |
| the `fonts` manifest field                                                                                     | `0.2.0`  |

Because a permission's entry moves whenever that permission gains a method,
an update of your widget can need a newer app than its previous version did
even when your code did not change. The stamp shows on the upload's success
screen and on your submissions page.


# Event reference (/docs/reference/events)



Widgets subscribe to events through the SDK's `on*` methods (`dd.time.onTick`,
`dd.system.onVitals`, ...); every one returns an unsubscribe function. Events
deliver only while the widget is mounted and holds the gating permission;
hidden widgets unmount entirely and receive nothing. A peek (every widget
concealed by the user) keeps widgets mounted, so events keep flowing.

## The topics [#the-topics]

| Topic                        | Needs                  | Payload                                                 | Delivery                                                                                                                                                                                                  |
| ---------------------------- | ---------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `time.tick`                  | `time`                 | `{ epochMs }`                                           | second-aligned wall clock                                                                                                                                                                                 |
| `settings.changed`           | nothing                | effective settings object                               | on every edit, including live previews from the settings dialog                                                                                                                                           |
| `theme.tokens`               | nothing                | full `--dd-*` token map                                 | on every theme change and every per-widget style edit (previews and the revert included), to every widget; the SDK re-applies it to `:root`                                                               |
| `cursor.hover`               | nothing                | `{ inside }`                                            | enter and leave over the widget-plus-pane footprint                                                                                                                                                       |
| `cursor.move`                | nothing                | `{ x, y }` viewport fractions                           | roughly 30 Hz while the cursor is inside                                                                                                                                                                  |
| `desktop.changed`            | `desktop`              | `{}` ping                                               | desktop item set changed; re-list                                                                                                                                                                         |
| `folders.changed`            | `folders`              | `{}` ping                                               | a granted folder changed; only to affected instances; debounced \~400 ms                                                                                                                                  |
| `launchers.drag`             | `launchers`            | `{ inside, count, kinds }`                              | targeted: an OS drag entered or left this instance's drop rect; emitted on target change only                                                                                                             |
| `launchers.drop`             | `launchers`            | `{ items }`                                             | targeted: the drop landed on this instance; the items are already in the widget's store                                                                                                                   |
| `launchers.changed`          | `launchers`            | `{}` ping                                               | the widget's store changed (a drop, a forget, an uninstall); every frame of the widget type                                                                                                               |
| `system.vitals`              | `system`               | `SystemVitals`                                          | 1 Hz sampler snapshot                                                                                                                                                                                     |
| `media.nowPlaying`           | `media`                | `NowPlaying`                                            | change-driven, not fixed-rate                                                                                                                                                                             |
| `audio.spectrum`             | `audio`                | `{ bins: number[] }`                                    | \~30 Hz while media plays (20 Hz low-spec)                                                                                                                                                                |
| `widgets.changed`            | `control`              | `{}` ping                                               | some approved instance's state changed; re-list                                                                                                                                                           |
| `popout.changed`             | manifest `popout`      | `{ active, side? }`                                     | open, reshape, close, including host force-closes; both documents                                                                                                                                         |
| `windows.changed`            | `taskbar` + kind `bar` | `{ windows }`, the full `dd.windows.list` snapshot      | change-driven: a window opened or closed, focus moved, a title changed, minimize state flipped, a window changed monitors; the driver for a taskbar's focus indicator                                     |
| `pins.changed`               | `taskbar` + kind `bar` | `{ pins }`, the full `dd.pins.list` snapshot            | change-driven: a pin made or removed, or the pinned order moved                                                                                                                                           |
| `bar.strip.changed`          | `taskbar` + kind `bar` | `{ items }`, the full `dd.bar.strip.list` snapshot      | change-driven: a row added or dropped, a pin made or removed, a move on any bar; the render order for a taskbar, to the bar document and its pane                                                         |
| `bar.flyout.changed`         | `taskbar` + kind `bar` | `{ open, key }`                                         | the preview flyout's real state, on every open, switch and close, whoever caused it; the bar document only                                                                                                |
| `systray.changed`            | `taskbar` + kind `bar` | `{ count, open }`                                       | the notification area's state, on every tray change and every open or close of the host list; the driver for a caret's pressed look. From app 0.4.2, `open` is true only on the bar whose caret opened it |
| `notificationCenter.changed` | `taskbar` + kind `bar` | `{ count, open, dnd, access }`, never the notifications | every change of the count, the access or the Do not disturb mode, and every open or close of the host's notification center; `open` is true only on the bar whose bell opened it. App 0.4.2+              |
| `popout.message`             | manifest `popout`      | whatever the other document sent                        | main and pane relay, up to 8 KB per message                                                                                                                                                               |
| `peers.drag`                 | `peers`                | `{ active, self }`                                      | drag lifecycle, to same-type frames                                                                                                                                                                       |
| `peers.drop`                 | `peers`                | `{ data, x, y }`                                        | targeted: only the drop target sees the payload                                                                                                                                                           |
| `peers.end`                  | `peers`                | `{ accepted, outside }`                                 | targeted to the drag's source                                                                                                                                                                             |

## Payload notes [#payload-notes]

**`settings.changed`** carries the effective settings. While the user tries
values in the settings dialog it fires with live-previewed (unsaved) values,
and once more with the stored values if they dismiss; treat every event the
same and re-apply. `dd.settings.get()` always returns the persisted state.

**`theme.tokens`** is delivered to every widget, subscribed or not, and the
SDK re-applies the map to `:root` automatically, so plain CSS needs nothing.
Per-widget style edits (the Style tab of your widget's settings window)
arrive the same way, live previews included. Subscribe (via
`dd.theme.onChange`) only when you resolve token values in JS; never cache
across it. Since app 0.4.0 theme colors fade on screen, and the handlers run
once the fade has settled (at once when there is no fade).

**`cursor.hover` and `cursor.move`** work for non-interactive widgets too:
the window stays click-through, you get the signal without capturing input.
While a popout is open the footprint is the bounding box of widget plus pane
(crossing the gap never flickers a leave), both documents receive `hover`,
and `move` goes to the document the cursor is actually over. For simple
element hover, skip the subscription: mark elements `data-dd-hover` and the
SDK toggles a `dd-hover` class.

**The `{}` pings** (`desktop.changed`, `folders.changed`, `launchers.changed`, `widgets.changed`)
carry no data on purpose: they tell you your last listing is stale, and the
matching `list` call is the read path.

**`system.vitals`**: the payload shape and its nullability contract are on
[the system page](/docs/reference/api/system#ddsystemonvitals). Every field
fails independently; render nulls as absent, not zero.

**`media.nowPlaying`**: shape, the session selection policy, and the local
position interpolation recipe are on
[the media page](/docs/reference/api/media-audio#ddmediaonnowplaying). Seed
with `dd.media.status()`.

**`audio.spectrum`**: 500 loudness bands, the silence marker frame, the
demand-driven capture rule, and the render-loop contract are on
[the audio section](/docs/reference/api/media-audio#ddaudio).

**`popout.changed` and `popout.message`**: lifecycle causes and the 8 KB
relay cap are on
[the popouts page](/docs/reference/api/widgets-together#ddpopout).

**`peers.*`**: the full drag lifecycle, including what `accepted` and
`outside` mean, is on
[the peers section](/docs/reference/api/widgets-together#ddpeers).

**The taskbar streams** (`windows.changed`, `pins.changed`,
`bar.strip.changed`, `bar.flyout.changed`, `systray.changed`) are the whole
truth for a bar: never move a focus indicator, a pressed caret or a row
from your own click, follow the stream. `dd.bar.store()` does that for
you; the shapes and the seed calls are on
[the bar widgets page](/docs/reference/api/bar-widgets).


# Manifest reference (/docs/reference/manifest)



`manifest.json` sits at the root of the widget folder and declares everything
about the widget up front: identity, what it may touch, its settings, and its
size on the grid. The loader is strict: unknown fields are rejected, and an
invalid manifest quarantines the widget with a reason instead of loading it.

A minimal manifest:

```json
{
  "$schema": "./.deskdash/manifest.schema.json",
  "id": "clock",
  "name": "Clock",
  "version": "0.1.0",
  "apiVersion": 1,
  "permissions": { "time": true },
  "defaultSize": { "w": 4, "h": 2 }
}
```

## Top-level fields [#top-level-fields]

| Field           | Type       | Required | Default              | Meaning                                                                                                                                                                                                                                                                                                                               |
| --------------- | ---------- | -------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$schema`       | string     | no       | none                 | Editor tooling metadata (the dev kit writes `./.deskdash/manifest.schema.json`). Accepted and ignored by the app.                                                                                                                                                                                                                     |
| `id`            | string     | yes      |                      | The widget id. Lowercase letters, digits, `.` and `-`, starting with a letter or digit. Must equal the folder name.                                                                                                                                                                                                                   |
| `name`          | string     | yes      |                      | Display name.                                                                                                                                                                                                                                                                                                                         |
| `version`       | string     | yes      |                      | The widget version. The app treats it as opaque; the marketplace requires dotted numbers (like `1.2.3`) and a strictly higher version on every update.                                                                                                                                                                                |
| `apiVersion`    | number     | yes      |                      | The bridge API contract the widget was written against (currently `1`). See [errors and versioning](/docs/reference/errors-versioning).                                                                                                                                                                                               |
| `minAppVersion` | string     | no       | none                 | "This widget needs DeskDash x.y.z or newer." A feature floor, checked before anything else, so a too-old app shows a friendly message instead of a parse error. The store stamps it on every published version ([how](/docs/reference/errors-versioning#minappversion-the-feature-floor)); leave it out unless you need a higher one. |
| `entry`         | string     | no       | `index.html`         | The widget document, a file inside the widget folder. Relative paths only: no leading `/`, no `..`, no `\`.                                                                                                                                                                                                                           |
| `description`   | string     | no       | `""`                 | Short description.                                                                                                                                                                                                                                                                                                                    |
| `author`        | string     | no       | `""`                 | Author name. Marketplace installs get this stamped by the store.                                                                                                                                                                                                                                                                      |
| `permissions`   | object     | no       | all off              | What the widget may touch. See [the permissions reference](/docs/reference/permissions).                                                                                                                                                                                                                                              |
| `interactive`   | boolean    | no       | `false`              | The widget wants real mouse input: while the cursor is over it, the window captures the mouse. Non-interactive widgets can never be clicked.                                                                                                                                                                                          |
| `contextMenu`   | boolean    | no       | `true`               | `false` opts the widget out of the right-click widget menu: in view mode right-clicks pass through it (to a widget beneath, or to the desktop's own menu), so a decorative widget is fully inert. Edit mode ignores the flag, so the widget stays configurable and removable there. App 0.2.3+.                                       |
| `settings`      | array      | no       | `[]`                 | Setting fields and preset names, mixed. The marketplace accepts up to 32 entries; a preset name counts as one. See below.                                                                                                                                                                                                             |
| `defaultSize`   | `{ w, h }` | no       | `{ "w": 4, "h": 2 }` | Size in grid cells when the widget is added. Each dimension 1..64.                                                                                                                                                                                                                                                                    |
| `minSize`       | `{ w, h }` | no       | `{ "w": 1, "h": 1 }` | The smallest the user can resize it to. Each dimension 0.25..64, in steps of 0.25. The app rounds it up to whole cells of the user's grid scale, never below one cell. A fraction is app 0.3.9+.                                                                                                                                      |
| `popout`        | object     | no       | none                 | Declares the popout capability. See below.                                                                                                                                                                                                                                                                                            |
| `fonts`         | object     | no       | none                 | Bundled font pins. App 0.2.0+. See below.                                                                                                                                                                                                                                                                                             |

## Settings [#settings]

Each `settings` entry is either a full field object or a preset name string;
the array accepts both, mixed:

```json
"settings": [
  { "key": "title", "type": "text", "label": "Title", "default": "Tasks" },
  "panelStyle"
]
```

The marketplace accepts up to 32 entries in `settings`; a preset name counts
as one entry, whatever it expands to.

### Field objects [#field-objects]

| Key           | Type                   | Required | Meaning                                                                                                                                                                                                             |
| ------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`         | string                 | yes      | The name your code reads the value under.                                                                                                                                                                           |
| `type`        | string                 | yes      | One of the field types below.                                                                                                                                                                                       |
| `label`       | string                 | yes      | Shown in the settings dialog.                                                                                                                                                                                       |
| `default`     | any                    | no       | The value before the user edits anything. Ignored for `secretRef`, `filePath`, `instances`, and `url` (see below).                                                                                                  |
| `options`     | string\[]              | no       | The choices for a `select`. A `select` with no `default` falls back to its first option.                                                                                                                            |
| `min`, `max`  | number                 | no       | Bounds for a `number`, and `max` caps the rows of a `list`. They are numbers, so `date` and `time` take no bounds.                                                                                                  |
| `placeholder` | string                 | no       | Hint text for empty `text`, `filePath` and `url` inputs, and for an empty `list` row. App 0.1.10+.                                                                                                                  |
| `pick`        | `"file"` or `"folder"` | no       | Adds a Browse button beside a `filePath` input and controls what it picks. App 0.1.10+.                                                                                                                             |
| `showIf`      | `{ "key", "equals" }`  | no       | Renders the field only while the sibling setting named `key` has the value `equals` (its default counts while unset). Use primitive values; a key that names no sibling fails open and the field shows. App 0.2.6+. |

The `date` and `time` types are app 0.3.4+, and `list` is app 0.3.7+. Older
apps do not know them and quarantine the widget; the store sets
`minAppVersion` for you.

### Field types [#field-types]

| Type        | The user gets                                                 | Your code gets                                                                                                                                   |
| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `text`      | a text input                                                  | string                                                                                                                                           |
| `number`    | a number input (`min`/`max` bounds)                           | number                                                                                                                                           |
| `boolean`   | a toggle                                                      | boolean                                                                                                                                          |
| `select`    | a dropdown of `options`                                       | string                                                                                                                                           |
| `color`     | a color picker                                                | string                                                                                                                                           |
| `date`      | a calendar picker                                             | a `"YYYY-MM-DD"` string, or `""` while unset; [`dd.dates.fromDayKey`](/docs/reference/api/environment#dddatesfromdaykey) makes it a local `Date` |
| `time`      | a clock picker                                                | an `"HH:MM"` string, or `""` while unset                                                                                                         |
| `list`      | text rows they add, reorder and remove                        | `string[]`, always an array; `[]` when they added nothing and the manifest declares no `default`                                                 |
| `secretRef` | a picker over their stored secrets                            | a name usable in `dd.http.fetch` secrets; never the value                                                                                        |
| `filePath`  | a path input (plus Browse with `pick`)                        | nothing directly; used as `settingKey` in `dd.files`, `dd.folders`, `dd.dialogs`, `dd.claudeUsage`                                               |
| `instances` | a picker over other widget instances                          | nothing directly; feeds `dd.widgets`                                                                                                             |
| `url`       | a web address input, flagged unless https (http on localhost) | nothing directly; used as `settingKey` in `dd.feeds`. App 0.3.9+                                                                                 |

The four consent-bearing types (`secretRef`, `filePath`, `instances`, `url`)
never inherit a manifest default. Their value grants access to something the
user owns or chooses (a credential, a file, other widgets, a web address), so only a value the user actively
picked counts; a published widget cannot ship pre-pointed at anyone's
private data.

### Setting presets [#setting-presets]

A preset bundles a recurring field (or field pair) with the code that applies
it, declared by name:

| Preset               | Expands to                                                                                                                                                                                                                                                                                    | Applied by                                                                                                                                                                                                                                                                                                              |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `panelStyle`         | a `Panel style` select (`none` / `solid` / `transparent` / `image`, default `solid`, key `panel`), plus from app 0.2.4 a `Background image` file picker (key `panelImage`) and an `Image fit` select (key `panelImageFit`), both shown in the settings dialog only while the style is `image` | `<dd-panel settings="panelStyle">` toggles its `transparent` / `bare` attributes and paints the image; zero widget code                                                                                                                                                                                                 |
| `accentColor`        | a `Use theme accent color` toggle (key `useAccent`, default on) plus a `Custom color` picker (key `color`)                                                                                                                                                                                    | your code, via `dd.theme.accent(settings)` or `dd.theme.bindAccent(cb)`; it has no component-side auto-apply                                                                                                                                                                                                            |
| `systray`            | a `Show the system tray` toggle (key `showTray`, default on)                                                                                                                                                                                                                                  | [`<dd-tray-caret settings="systray">`](/docs/reference/components#dd-tray-caret) shows or hides the caret; zero widget code                                                                                                                                                                                             |
| `notificationCenter` | a `Show notifications` toggle (key `showNotifications`, default on); app 0.4.2+                                                                                                                                                                                                               | [`<dd-notification-bell settings="notificationCenter">`](/docs/reference/components#dd-notification-bell) shows or hides the bell (and closes its window when turned off); zero widget code                                                                                                                             |
| `pins`               | a `Show pinned apps` toggle (key `showPins`, default on)                                                                                                                                                                                                                                      | [`<dd-apps settings="pins">`](/docs/reference/components#dd-apps-and-dd-app) shows or hides the launchers (and never asks the host for pins while off); zero widget code. Bar widgets only, app 0.3.0+                                                                                                                  |
| `weekStart`          | a `Week starts on` select (`monday` / `sunday`, key `weekStart`); app 0.3.4+                                                                                                                                                                                                                  | your code, via [`dd.dates.startOfWeek(d, settings.weekStart)`](/docs/reference/api/environment#dddatesstartofweek), which takes the string as stored                                                                                                                                                                    |
| `textScale`          | a `Text size` select (`tiny` / `small` / `medium` / `large` / `huge`, key `textSize`); app 0.3.4+                                                                                                                                                                                             | your code mirrors it onto the body (`document.body.dataset.textScale = value`); the base stylesheet turns it into the `--dd-text-scale` multiplier, which the [autoscale skeleton](/docs/guides/theming#autoscale-readouts-that-follow-the-widgets-size) and any `calc(... * var(--dd-text-scale, 1))` of your own ride |

An unknown preset name quarantines the manifest. To keep a preset's behavior
but change its default, declare the field inline with the same `key` and your
default, and still put the preset name on the component; it keys off the
setting value regardless of how the field was declared. Declare all of the
preset's fields when you do, and drop the preset name from the manifest:
expansion appends without deduplicating, so keeping both lists a field twice.

Published manifests carry presets already expanded (the store renders the
field list from them), so a preset gaining a field reaches installed widgets
only through a new widget version.

## Bar [#bar]

```json
"kind": "bar",
"bar": {
  "thickness": 64, "reserve": 64, "edges": ["left", "right"], "allowOverlay": true,
  "autoHide": { "hideDelay": 150, "revealBand": 2, "slide": 140 }
}
```

A `kind: "bar"` manifest renders as the taskbar instead of a grid widget
and must carry a `bar` block. `thickness` (logical px, 24 to 128) is the
window's extent away from its edge, and the user may scale it (the Size
slider in the bar's settings dialog, 50% to 200%), so treat it as the design
size and lay out against the viewport; `reserve` (0 to thickness, default =
thickness) is the strip the OS reserves, and a smaller value leaves
transparent headroom for hover animations; `edges` (default `["bottom"]`;
`"left"` and `"right"` make a vertical bar) lists the edges the bar can
dock to, the first being the default and several giving the user a Side
choice; `allowOverlay` (default `true`) lets the user drop the reservation;
`autoHide` (optional, every field optional) tunes the user's auto-hide:
`hideDelay` ms before the bar hides (0 to 2000, default 150), `revealBand`
logical px on the screen edge that reveals it (1 to 8, default 2), `slide`
ms for the slide (0 to 600, default 140). Side, auto-hide and overlay are
picked in the bar's own settings dialog. Bars are app 0.3.0+ (the store
sets `minAppVersion`). The [bar widgets
reference](/docs/reference/api/bar-widgets) covers the surface.

A bar declares no explicit `filePath`, `secretRef`, `instances` or `url` field: the
bar's settings dialog never offers those types, so the user could never set
one, and the loader, the upload validator and the CI check all refuse the
manifest (app 0.3.7 or newer; older apps loaded it and never showed the
field). Preset names are exempt: `panelStyle` keeps its image field on a bar,
and its `image` option stays unavailable there (`solid` and `transparent`
work).

## Popout [#popout]

```json
"popout": { "max": { "w": 8, "h": 6 }, "entry": "pane.html" }
```

Presence declares that the widget may temporarily extend past its grid rect
via [dd.popout.open](/docs/reference/api/widgets-together#ddpopoutopen). It
is a geometry capability like `interactive`, not a privacy permission: the
pane is a second sandboxed document with the same grants. A `kind: "bar"`
widget declares it without `max` (its pane renders beside the strip, capped
to the display).

| Key     | Meaning                                                                          |
| ------- | -------------------------------------------------------------------------------- |
| `max`   | Caps the pane size, in base grid cells like `defaultSize`. Each dimension 1..64. |
| `entry` | The pane document, a file in the widget folder (same path rules as `entry`).     |

## Fonts [#fonts]

Optional, and app 0.2.0+ (older apps do not know the key; the store sets
`minAppVersion`). Pins a font onto one or more of the three face tokens, in this
widget's documents only:

```json
"fonts": { "use": { "display": "audiowide" } }
```

`use` takes any of `sans`, `display`, `mono`. Values are bundled catalog ids
(`audiowide`, `nunito`, `nunito-sans`, and from app 0.2.4 the pixel set
`pixelify-sans`, `press-start-2p`, `silkscreen`; an unknown id quarantines
the manifest, so the pixel ids need app 0.2.4+) or a
`self:` reference to a family the widget ships itself:

```json
"fonts": {
  "bundle": [{ "id": "heading", "files": [{ "src": "fonts/mine.woff2", "weight": "400" }] }],
  "use": { "display": "self:heading" }
}
```

| Key                       | Meaning                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| `bundle`                  | Families shipped in the widget folder. Each has an `id` (for `self:` references) and 1..6 `files`. |
| `bundle[].files[].src`    | Path inside the widget folder, charset `A-Za-z0-9._/-`.                                            |
| `bundle[].files[].weight` | One value or a variable range (`"200 1000"`), each within 1..1000. Default `"400"`.                |
| `bundle[].files[].style`  | `"normal"` (default) or `"italic"`.                                                                |
| `use`                     | Optional pins: `sans`, `display`, `mono`, each a catalog id or `self:<bundle id>`.                 |

Rules, enforced at manifest load and again by the marketplace validators:
woff2 only (sniffed by magic number, never by extension); at most 4 families,
6 files per family, 2 MB per file, and 4 MB of fonts per widget. The host
generates all resulting CSS itself and names families by index, so nothing a
manifest says ever reaches a stylesheet.

The pin wins over themes and user overrides inside your documents. Your CSS
keeps using the token (`font-family: var(--dd-font-display)`). Two notes:
`dd.theme.tokens()` does not see pins, `dd.ui.token()` does, so canvas
drawing must resolve faces through
[dd.ui.token()](/docs/reference/api/ui#dduitoken). Every bundled catalog face
also exists as a convenience var (`--dd-font-audiowide`, `--dd-font-nunito`,
`--dd-font-nunito-sans`, `--dd-font-pixelify-sans`,
`--dd-font-press-start-2p`, `--dd-font-silkscreen`), so a `select` setting
can offer a font choice without a literal family name in widget code.

Publishing note: the marketplace requires every font-shaped file in the
folder to be declared in `fonts.bundle`, holds font-carrying submissions for
licensing review, and requires the font's license to permit embedding and
redistribution, with the license text included in the widget.

## Validation [#validation]

What the loader enforces, in order:

1. `minAppVersion`, when present, must be a string that parses as `x.y.z`,
   and the running app must satisfy it. A too-old app reports "needs
   DeskDash x.y.z or newer".
2. `apiVersion` newer than the host reports "requires a newer dashboard".
3. The strict parse: unknown fields anywhere are rejected.
4. `id` must match `^[a-z0-9][a-z0-9.-]*$` and equal the folder name.
5. `entry` (and `popout.entry`) must be a relative path inside the widget
   folder: no leading `/`, no `..`, no `\`.
6. Sizes (`defaultSize`, `popout.max`) must each be 1..64 cells per
   dimension, and `minSize` 0.25..64 in steps of 0.25; `popout.max` is required for grid widgets and absent on
   bars. `bar.edges` is a non-empty list of distinct edges; `bar.autoHide`
   fields must sit in their ranges (`hideDelay` 0 to 2000, `revealBand` 1
   to 8, `slide` 0 to 600).
7. A `kind: "bar"` manifest may not declare an explicit `filePath`,
   `secretRef` or `instances` field (app 0.3.7 or newer), or a `url` field
   (app 0.3.9 or newer); preset names are exempt.
8. `fonts`, when present, is validated file by file (existence, format,
   caps) before the widget loads. Fail closed: a bad font spec quarantines
   the manifest.

A quarantined widget shows up with its reason in the widget list; it never
takes the rest of the dashboard down.


# Permissions reference (/docs/reference/permissions)



Everything is denied by default. A widget declares what it needs in the
manifest's `permissions` object, and the user consents at install time,
permission by permission. Three rules shape the system:

* **Consent can only narrow.** What the user granted is intersected with the
  manifest; nothing can escalate past what the manifest declared, and the
  user may grant less.
* **Updates re-prompt only for additions.** An update that adds a permission
  asks again; removals apply silently.
* **Ask for what you use.** Every grant is shown to the user in plain
  language (the wording below). A widget requesting more than it visibly
  needs reads as suspicious in review and to users.

## The permissions [#the-permissions]

The wording in the second column is exactly what the marketplace shows the
user at install time.

| Permission       | Shown to the user as                                                                                       | Unlocks                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `time`           | Reads the current time from the dashboard's clock                                                          | [dd.time](/docs/reference/api/environment#ddtime), the `time.tick` event                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `storage`        | Saves its own data inside the dashboard                                                                    | [dd.storage](/docs/reference/api/settings-storage#ddstorage)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `files`          | Reads files or folders you explicitly pick for it                                                          | [dd.files.read](/docs/reference/api/settings-storage#ddfiles) of user-set `filePath` settings                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `desktop`        | Integrates with your desktop (icons, wallpaper area)                                                       | [dd.desktop](/docs/reference/api/desktop-apps#dddesktop), `desktop.changed`, [dd.dialogs.pickDesktopItems](/docs/reference/api/notifications-dialogs#dddialogspickdesktopitems)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `system`         | Reads system stats like CPU, memory and battery                                                            | [dd.system.status / onVitals](/docs/reference/api/system#ddsystem), the `system.vitals` stream                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `systemControls` | Changes system settings such as volume, sound devices, Wi-Fi, Bluetooth and brightness                     | the [dd.system](/docs/reference/api/system#ddsystem) write surface: volume, mute, the microphone, the radio switch, Wi-Fi, openSettings, the audio device list and switch, the per-app mixer, the paired Bluetooth list and connect, display brightness. Useless without `system`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `media`          | Sees what music or video is playing                                                                        | [dd.media.status / onNowPlaying](/docs/reference/api/media-audio#ddmedia), the `media.nowPlaying` stream                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `mediaControls`  | Controls media playback                                                                                    | the [dd.media](/docs/reference/api/media-audio#transport-controls) transport verbs. Useless without `media`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `audio`          | Reads audio levels (for visualizations)                                                                    | the [audio.spectrum](/docs/reference/api/media-audio#ddaudio) stream. Loudness bands only, never raw audio                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `control`        | Controls other widgets you grant it access to                                                              | [dd.widgets](/docs/reference/api/widgets-together#ddwidgets) over user-approved targets, `widgets.changed`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `peers`          | Talks to other widgets of the same type                                                                    | [dd.peers](/docs/reference/api/widgets-together#ddpeers) drag and drop                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `systemApps`     | Opens system apps you pick for it                                                                          | [dd.apps](/docs/reference/api/desktop-apps#ddapps), catalog keys only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `claudeUsage`    | Reads your local Claude Code usage summary                                                                 | [dd.claudeUsage.status](/docs/reference/api/data-services#ddclaudeusage), aggregated only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `notifications`  | Shows you reminders, even when the widget is hidden                                                        | [dd.notify.sync](/docs/reference/api/notifications-dialogs#ddnotify)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `power`          | Shuts down, restarts (also into advanced startup), sleeps, locks or signs out of this PC                   | [dd.power](/docs/reference/api/system#ddpower)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `folders`        | Lists and opens items in folders you pick for it                                                           | [dd.folders](/docs/reference/api/desktop-apps#ddfolders), `folders.changed`, [dd.dialogs.pickFolderItems](/docs/reference/api/notifications-dialogs#dddialogspickfolderitems)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `games`          | Sees the games installed on this PC and can start them                                                     | [dd.games](/docs/reference/api/desktop-apps#ddgames), opaque ids and local cover art                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `taskbar`        | Reads your taskbar: open windows, pinned apps, the notification area and how many notifications you have   | the bar-only surface (`kind: "bar"` widgets): [dd.windows](/docs/reference/api/bar-widgets#ddwindows), [dd.pins](/docs/reference/api/bar-widgets#ddpins), [dd.bar.strip](/docs/reference/api/bar-widgets#ddbarstrip) (with its one write, a reorder on the user's drag), [dd.bar.flyout](/docs/reference/api/bar-widgets#ddbarflyout), [dd.systray](/docs/reference/api/bar-widgets#ddsystray), [dd.notificationCenter](/docs/reference/api/bar-widgets#ddnotificationcenter) (0.4.2+: a count and open and close; the notifications stay in a host-drawn window), [dd.bar.openStartMenu](/docs/reference/api/bar-widgets#ddbaropenstartmenu), [dd.bar.openQuickSettings](/docs/reference/api/bar-widgets#ddbaropenquicksettings) and [dd.bar.openNotifications](/docs/reference/api/bar-widgets#ddbaropennotifications) |
| `launchers`      | Opens the files, folders and apps you drop on it                                                           | [dd.launchers](/docs/reference/api/desktop-apps#ddlaunchers), the `launchers.drag`, `launchers.drop` and `launchers.changed` events. Grid widgets only: the drop gesture is the consent, and the widget only ever sees opaque ids                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `feeds`          | Reads news feeds from web addresses you enter in its settings and opens their articles when you click them | [dd.feeds.fetch](/docs/reference/api/data-services#ddfeeds), the address the user typed into a `url` setting. The host fetches and parses the feed; only titles, links and dates reach the widget. [dd.feeds.open](/docs/reference/api/data-services#ddfeedsopen) (0.3.11+) opens those links from a click, no `links` grant needed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |

## The scoped grants [#the-scoped-grants]

`http` and `links` are objects, not booleans, and their scope is part of the
consent.

### http [#http]

```json
"permissions": { "http": { "hosts": ["api.open-meteo.com"], "rateLimit": 60 } }
```

* `hosts`: exact hostnames the widget may fetch through
  [dd.http.fetch](/docs/reference/api/data-services#ddhttpfetch). Shown to
  the user as: "Talks to the internet, only to:" followed by the list. No
  wildcards; every hop of a redirect is re-checked.
* `rateLimit`: requests per minute, optional. Omitted means the host default
  of 30; values are clamped to 120. Never shown at install.

### links [#links]

```json
"permissions": { "links": { "prefixes": ["steam://run/"] } }
```

* `prefixes`: URL prefixes
  [dd.links.open](/docs/reference/api/core#ddlinksopen) may launch, each a
  full `scheme://...` string. Shown to the user as: "Can open these link
  types on your PC:" followed by the list. Empty means no access, and
  dangerous schemes are refused at call time no matter what a manifest
  declares.

## Read and control pairs [#read-and-control-pairs]

Two capabilities are split so that reading never implies acting:

* `system` reads vitals; `systemControls` changes volume, the
  microphone, the radios, Wi-Fi, the sound device, per-app volume,
  Bluetooth connections and brightness, and opens settings panels. The
  lists that name your apps, devices and displays are `systemControls`
  reads too. Controls without `system` are useless.
* `media` reads now-playing; `mediaControls` drives playback.

`power` is deliberately its own permission rather than part of
`systemControls`: granting a volume slider is not granting the ability to
turn the machine off.

## Version floors [#version-floors]

Some permissions did not exist in the earliest app releases, and others
gained methods later. You do not declare anything for this: the store
stamps every new version's `minAppVersion` from the release that last
changed each permission it requests (see
[the feature floor](/docs/reference/errors-versioning#minappversion-the-feature-floor)).
The entries above the oldest supported app, today:

| Permission                                           | Last changed in |
| ---------------------------------------------------- | --------------- |
| `audio`                                              | `0.3.0`         |
| `games`                                              | `0.3.2`         |
| `desktop`, `folders`, `links`, `power`, `systemApps` | `0.3.7`         |
| `launchers`                                          | `0.3.9`         |
| `feeds`                                              | `0.3.11`        |
| `system`, `systemControls`, `taskbar` (bar widgets)  | `0.4.2`         |

## Consent-bearing settings [#consent-bearing-settings]

Four setting types are part of the permission story:
`filePath` (which file or folder), `secretRef` (which credential),
`instances` (which other widgets), and `url` (which web address a feed
comes from). Their manifest defaults are always
ignored; only what the user actively picked counts. See
[the manifest reference](/docs/reference/manifest#field-types). Bar widgets
cannot declare these fields at all: the manifest refuses them, see
[Bar](/docs/reference/manifest#bar).

One API rides this model with no grant of its own:
[dd.image.load](/docs/reference/api/settings-storage#ddimageload) returns
the image the user picked in a `filePath` setting as a `data:` URL. The
picked file IS the consent; the method can read nothing else, and only
sniffed image bytes with an 8 MB cap ever cross the bridge.

For the containment model behind all of this, see
[How the sandbox works](/docs/concepts/security).


# Token reference (/docs/reference/tokens)



Every visual in DeskDash rides the `--dd-*` design tokens: the shell, the
built-in widgets, and yours. Style with tokens and your widget follows every
theme automatically; hardcode a color and it sticks out the moment the user
switches themes.

The values below are the defaults (a dark baseline). Themes may change any
of them at runtime: plain CSS picks changes up automatically, and JS that
resolved a token (canvas, inline SVG) should re-read via
[dd.ui.token()](/docs/reference/api/ui#dduitoken) inside
[dd.theme.onChange](/docs/reference/api/environment#ddthemeonchange).

## Surfaces [#surfaces]

| Token                      | Default                     | Used for                                            |
| -------------------------- | --------------------------- | --------------------------------------------------- |
| `--dd-bg`                  | `transparent`               | the widget document background; keep it transparent |
| `--dd-surface`             | `rgba(17, 22, 31, 0.72)`    | the standard panel surface                          |
| `--dd-surface-strong`      | `rgba(13, 17, 24, 0.92)`    | emphasized surfaces (popovers, panes)               |
| `--dd-surface-transparent` | `rgba(17, 22, 31, 0.35)`    | the barely-there glass panel style                  |
| `--dd-border`              | `rgba(148, 163, 184, 0.16)` | hairline borders                                    |

`--dd-surface-transparent` is transparent by contract: widgets using it
should read as floating on the desktop, and the user's surface-opacity
setting deliberately leaves it alone (that knob solidifies the solid
panels).

## Text [#text]

| Token             | Default   |
| ----------------- | --------- |
| `--dd-text`       | `#e7edf5` |
| `--dd-text-muted` | `#94a3b8` |

## Accent and status [#accent-and-status]

| Token                  | Default   | Used for             |
| ---------------------- | --------- | -------------------- |
| `--dd-accent`          | `#7dd3fc` | the theme accent     |
| `--dd-accent-strong`   | `#38bdf8` | emphasized accent    |
| `--dd-accent-contrast` | `#0b1220` | text on accent fills |
| `--dd-success`         | `#4ade80` | good / done          |
| `--dd-warning`         | `#fbbf24` | caution              |
| `--dd-danger`          | `#f87171` | errors / critical    |

## Chart palette [#chart-palette]

A data-series palette, themable as a set. Use them in order.

| Token          | Default            |
| -------------- | ------------------ |
| `--dd-chart-1` | `#6ee7a0` (green)  |
| `--dd-chart-2` | `#6cb2f7` (blue)   |
| `--dd-chart-3` | `#b18af8` (purple) |
| `--dd-chart-4` | `#f5a962` (orange) |
| `--dd-chart-5` | `#5fd4d0` (teal)   |

## Font faces [#font-faces]

| Token               | Default                                                       |
| ------------------- | ------------------------------------------------------------- |
| `--dd-font-sans`    | `"Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif` |
| `--dd-font-display` | `var(--dd-font-sans)`                                         |
| `--dd-font-mono`    | `"Cascadia Mono", Consolas, monospace`                        |

`--dd-font-display` is the headline face: themes point it at a character
font while everything else stays sans. The `dd-text` variants `title` and
`value` use it. A widget with a
[manifest font pin](/docs/reference/manifest#fonts) gets these rewritten in
its own documents, and each bundled catalog face also exists as a
convenience var: `--dd-font-audiowide`, `--dd-font-nunito`,
`--dd-font-nunito-sans`, `--dd-font-pixelify-sans`,
`--dd-font-press-start-2p`, `--dd-font-silkscreen`.

## Type scale [#type-scale]

`<dd-text size="...">` maps to these; any widget can override.

| Token                | Default |
| -------------------- | ------- |
| `--dd-font-size-xs`  | `11px`  |
| `--dd-font-size-sm`  | `12px`  |
| `--dd-font-size-md`  | `14px`  |
| `--dd-font-size-lg`  | `18px`  |
| `--dd-font-size-xl`  | `24px`  |
| `--dd-font-size-2xl` | `32px`  |

## Font weights [#font-weights]

`<dd-text weight="...">` maps to these.

| Token                       | Default |
| --------------------------- | ------- |
| `--dd-font-weight-normal`   | `400`   |
| `--dd-font-weight-medium`   | `500`   |
| `--dd-font-weight-semibold` | `600`   |
| `--dd-font-weight-bold`     | `700`   |

## Tracking [#tracking]

| Token                | Default  | Used for                                                        |
| -------------------- | -------- | --------------------------------------------------------------- |
| `--dd-tracking-wide` | `0.08em` | uppercase label tracking (the one letter-spacing in the system) |

## Radii [#radii]

| Token              | Default | Used for          |
| ------------------ | ------- | ----------------- |
| `--dd-radius-sm`   | `6px`   | small controls    |
| `--dd-radius-md`   | `10px`  | buttons, chips    |
| `--dd-radius-lg`   | `16px`  | panels            |
| `--dd-radius-full` | `999px` | pills and circles |

## Spacing [#spacing]

| Token          | Default |
| -------------- | ------- |
| `--dd-space-1` | `4px`   |
| `--dd-space-2` | `8px`   |
| `--dd-space-3` | `12px`  |
| `--dd-space-4` | `16px`  |
| `--dd-space-6` | `24px`  |

There is no `--dd-space-5`; the scale jumps from 16 to 24.

## Elevation and dimming [#elevation-and-dimming]

| Token              | Default                          |
| ------------------ | -------------------------------- |
| `--dd-shadow`      | `0 8px 24px rgba(0, 0, 0, 0.35)` |
| `--dd-opacity-dim` | `0.6`                            |

## Motion [#motion]

Durations plus the house easing (fast start, soft landing). Component
transitions ride these, and themes can retime them. The OS reduced-motion
preference disables the built-in motion regardless.

The user's Animation speed setting (Settings > General: off, fast, normal,
slow) multiplies both durations after the theme, so a theme's retime and the
user's choice compose, and Off makes everything instant. You read the result
through the tokens like any other value; there is nothing to declare.

| Token            | Default                      |
| ---------------- | ---------------------------- |
| `--dd-anim-fast` | `140ms`                      |
| `--dd-anim-slow` | `260ms`                      |
| `--dd-ease`      | `cubic-bezier(0.2, 0, 0, 1)` |

## Stacking [#stacking]

| Token           | Default | Used for                            |
| --------------- | ------- | ----------------------------------- |
| `--dd-z-popout` | `10`    | popouts paint above their neighbors |

## Autoscale [#autoscale]

For widgets whose one big readout should scale with the widget: the target
font size is a fraction of the container's smaller edge, capped at a
maximum. Overridable per widget by setting the vars on any element inside
the container.

| Token                 | Default   |
| --------------------- | --------- |
| `--dd-autoscale-size` | `36cqmin` |
| `--dd-autoscale-max`  | `44px`    |

## How themes layer [#how-themes-layer]

The host computes the effective map as defaults, then the active theme's
tokens, then the user's own overrides, and broadcasts the result to every
widget document (the `theme.tokens` event; the SDK re-applies it to
`:root`). One more layer is per widget: a user can override any token on a
single widget instance from the Style tab of its settings window, and the
shell merges that map over the effective one for that instance's documents
only. Your widget sees it as a different `var()` value, previews included,
and `dd.theme.tokens()` returns the same merged map. Widgets never read
theme files: the current map is always just `var(--dd-*)` in CSS or
`dd.ui.token()` in JS.


# Bar widgets (/docs/reference/api/bar-widgets)



Some methods exist only for a `kind: "bar"` widget that also holds the
[`taskbar`](/docs/reference/permissions) permission. The bridge refuses the
whole surface for a regular desktop widget before permissions are even read,
so "a desktop widget can never see your open windows" stays a one-line
guarantee.

This page is the reference. [Building a taskbar](/docs/guides/taskbars)
explains what a bar is, what the host does for it, and how to make one.

Most bars never call any of it directly: the
[bar components](/docs/reference/components#dd-apps-and-dd-app)
(`<dd-apps>`, `<dd-bar-button>`, `<dd-tray-caret>`) carry the behavior on
[dd.bar.store](#ddbarstore), and a bar differs in its CSS. The primitives
under them stay public for a bar that draws its own buttons: the strip (the
host-owned order), the windows, the pins, the preview flyout, the tray
mirror, the start-menu tap and the app menu.

Two rules hold everywhere on this page. Every id is an opaque host token: a
stale one rejects with `NOT_FOUND`, and the fix is always to re-list from
the matching stream. A popup anchor is an element (the SDK measures it) or
a rect in the bar document's CSS px; `y` and `h` matter beside a side bar,
where every popup opens to the side of the strip.

A bar has two documents, the strip and its pane, and not every verb serves
both. Bar window only: `dd.bar.flyout.*`, `dd.bar.appMenu`,
`dd.bar.openStartMenu`, `dd.bar.openQuickSettings`,
`dd.bar.openNotifications`, `dd.systray.open` and `close`, `dd.bar.strip.move`,
`dd.popout.open` and the region reporting. Both, under the cursor-inside
gate (the bar window, or the pane and its anchor button), from app 0.3.7:
`dd.settings.open`, `dd.links.open`, `dd.dialogs.pickDesktopItems` and
`dd.power.run`; older apps refuse them from a bar. Both, with no gate: the
streams, `dd.bar.strip.list`, the `dd.windows` reads and `dd.pins.list`.

## Types [#types]

```ts
interface OsWindow {
  /** Valid only while the window is in the current list. */
  id: string;
  title: string;
  /** Stable per-app icon key for dd.windows.icons; empty when the host
   *  could not read the process. */
  iconKey: string;
  /** The id of the strip row that claims this window (a pin's id, or the
   *  id of the row a running app got), stamped by the host before either
   *  list goes out and fixed while the window is listed. Empty only on a
   *  host older than 0.3.5. */
  appKey: string;
  focused: boolean;
  minimized: boolean;
  /** Stable display id of the window's monitor (its restore monitor while
   *  minimized). The host already cuts the list to what this bar shows, so
   *  a bar never filters on it. */
  displayId: string;
}

interface PinnedApp {
  /** The only handle dd.pins.launch accepts; stable across rescans and
   *  across unpin and re-pin of the same app. */
  id: string;
  name: string;
  /** Equals id: the host stamps it onto every window the pin claims. */
  appKey: string;
  /** Shares the windows icon space: a pinned and running app is one cache entry. */
  iconKey: string;
}

interface StripRow {
  /** The row's identity everywhere: the appKey, the iconKey, the id that
   *  pins.launch and strip.move take. Survives pin, unpin and re-pin. */
  id: string;
  /** A pin (stays when its windows close) or a transient row (drops a
   *  moment after its last window closes). */
  pinned: boolean;
  /** The pin's display name; empty for a transient row (show the window title). */
  name: string;
  /** Equals id. */
  iconKey: string;
}

interface BarEntry {
  key: string;
  /** The running windows; empty for a launcher. */
  wins: OsWindow[];
  /** The pin behind the entry; null for an unpinned running app. */
  pin: PinnedApp | null;
}

interface BarFlyoutStyle {
  /** Preview width in CSS px, 120 to 320; height follows 16:9. */
  previewW?: number;
  /** Panel corner radius in px, 0 to 24. */
  radius?: number;
  /** Gap between cards in px, 0 to 24. */
  gap?: number;
  /** Panel background, #rrggbb or #rrggbbaa only. */
  surface?: string;
  /** Panel border color, #rrggbb or #rrggbbaa only. */
  border?: string;
  showTitle?: boolean;
  showClose?: boolean;
}

/** A popup anchor by hand: CSS px of the bar viewport. */
type BarAnchor = { x: number; y?: number; w: number; h?: number };
```

One key ties the three lists together. A strip row's `id` is the `appKey`
the host stamps onto every window the row claims, the `iconKey` for
[dd.windows.icons](#ddwindowsicons), the id [dd.pins.launch](#ddpinslaunch)
takes for a pinned row, and the id [dd.bar.strip.move](#ddbarstripmove)
takes. Attaching windows to rows is a plain equality, and a bar never keeps
an order of its own.

## dd.bar.store [#ddbarstore]

```ts
dd.bar.store(): BarStore
```

The headless taskbar state, one lazy instance per bar document. Signals
(`dd.ui.signal`, read `.value` inside an `effect`, `.peek()` outside):
`strip` (the host's ordered rows, see [dd.bar.strip](#ddbarstrip)) and
`windows` (the host snapshots), `pins` (the pinned rows as `PinnedApp`, a
projection of the strip), `entries` (the render list: the strip with the
windows attached, one entry per row that has something to show, in strip
order, or in the local order during a drag or a move in flight), `flyout`
(`{ open, key }`, from `bar.flyout.changed` only), `pinsEnabled`,
`sorting`, `lowMotion` (low-spec host or reduced motion), `edge`; per key,
`icon(iconKey)` (the data URI, fetched in batches on first read; a key the
host has no icon for stays `undefined` and is never asked again) and
`launching(key)`. `entryByKey(key)` returns the entry behind a key, or
`undefined`. `ready` resolves after the seed (the strip first, then the
windows), with every listener already registered.

Actions: `activate(key, anchorEl)` is the click tree (a launcher launches
once, behind a lock that holds until its window lands or 6 s; a single
window focuses, or minimizes when it is the focused one; a stack toggles the
preview flyout anchored on the element), with `launch`, `openFlyout`,
`closeFlyout` and `toggleFlyout` as its pieces; `hoverEnter(key, el)` and
`hoverLeave()` run the hover dwell when `configure({ previews: "hover" })`
is set (a sweep opens nothing, a switch while open waits the same dwell,
leaving a button closes nothing); `pointerEntered()` and `pointerLeft()`
are the debounced advisory close for the bar segment; `enablePins(bool)` is
the pins gate (whether pinned launchers render; the host keeps its pins
either way); `beginSort(key)`, `previewOrder(keys)`, `move(key, before)`
and `endSort()` are a drag's four moments (the flyout closes and hover
pauses at `beginSort`, `previewOrder` keeps the pointer's order across host
emits, `move` shows the drop at once and sends `bar.strip.move`, the host's
next emit confirms it or a rejection rolls it back within a second,
`endSort` is a release without a move); `setFlyoutStyle` sets the style
every open sends; `menuSubject(entry)` is the app-menu subject.
`configure(opts)` takes `previews` (`"click"`, the default, or `"hover"`),
`dwell` (ms of continuous hover before a preview opens, default 400) and
`flyoutStyle`. `hooks` is the seam the components use to raise their
cancelable events; leave it alone.

The `<dd-apps>` / `<dd-app>` / `<dd-bar-button>` components render from it
and hold no behavior of their own. A bar that draws its own buttons reads
and calls the same thing.

## dd.bar.strip [#ddbarstrip]

The strip (app 0.3.5+; the store sets `minAppVersion`) is the
host-owned order of the taskbar: ONE list of rows, where a row is a pinned
app or a running app nobody pinned, the same for every bar on every
display. Which rows show still follows each bar's own window list: when
the user shows taskbar apps only where the window is open, a running row
appears only on the bars that list its windows, and pinned rows appear on
every bar. It is the user's order. Pins keep their place across restarts
(they live in the app config); running rows keep theirs while they run and
append at the end when they first appear, exactly like the Windows taskbar.
Pin and unpin happen in place: a running app keeps its slot when the user
pins it, a pinned app keeps its slot when unpinned while it runs, and an
unpin with no window drops the row.

Every window in `dd.windows.list` carries its row's id as `appKey`, stamped
by the host before either list goes out, so attaching windows to rows is a
plain equality and a bar never keeps an order of its own.

### dd.bar.strip.list() [#ddbarstriplist]

```ts
dd.bar.strip.list(): Promise<{ items: StripRow[] }>
```

The rows in strip order. Seed with this, then follow `onChanged`. A row
with `pinned: false` and no window in the window list is in the host's
short grace after its last window closed (its id stays valid for a
relaunch): render nothing for it. A bar's pane may call this too.

### dd.bar.strip.move() [#ddbarstripmove]

```ts
dd.bar.strip.move(id: string, before: string | null): Promise<void>
```

Moves row `id` in front of row `before`, or to the end of the strip when
`before` is `null`. Any row goes to any slot: a running app between two
pins, a pin after a running app. This is the one write a bar has into
host-owned state, so it is gated like the app menu: the bar document only
(never a pane), never in edit mode, and only while the cursor is over the
bar (a drop lands under it). Only host-issued ids cross; a stale `id` or
`before` rejects with `NOT_FOUND` (re-list on `onChanged`). The result comes
back through `onChanged` to every bar, and through `dd.pins.onChanged` when
the pinned order changed. The store does this for you: `<dd-apps sortable>`
turns a drag into `move`, with the drop shown at once and the stream
confirming it.

### dd.bar.strip.onChanged() [#ddbarstriponchanged]

```ts
dd.bar.strip.onChanged(cb: (p: { items: StripRow[] }) => void): () => void
```

Change-driven full snapshot: a row added (an app nobody pinned opened its
first window) or dropped (its grace ran out), a pin made or removed, a move
(yours or another bar's, on any display). Attaching windows to rows never
emits here; `dd.windows.onChanged` carries that. Reaches a bar's pane too.

## dd.bar.entries [#ddbarentries]

```ts
dd.bar.entries(windows: OsWindow[], pins: PinnedApp[]): BarEntry[]
```

The pure two-list merge, for a bar that reads `dd.windows` and `dd.pins`
itself: pins first in pin order, each merged with its running group when
the host stamped the pin's id onto the windows (`appKey` equality), a
launcher otherwise; then the unpinned running groups in the host's
first-seen order. Windows without an `appKey` stay their own entry. A merged
entry and its launcher share one key, so one DOM node can carry launcher,
running, launcher across a launch and a quit. It carries no user order:
the store's `entries` signal renders from the strip instead. The shapes are
in [Types](#types).

## dd.windows [#ddwindows]

The open top-level OS windows. Needs `taskbar` on a bar-kind widget; a
regular widget's call is refused before its permissions are read.

### dd.windows.list() [#ddwindowslist]

```ts
dd.windows.list(): Promise<{ windows: OsWindow[] }>
```

Every open top-level window, unstacked, in first-seen order: the order is
stable across focus changes and new windows append, so the list never
leaks z-order and buttons never jump. Each window's `appKey` is the id of
the strip row that claims it (by AppUserModelID when the window carries
one, else by exe; every UWP window shares one host process, which is why
exe alone would misgroup them). A new window is held out of the list for
up to about 300 ms while that identity resolves, so the key never changes
under a listed window. Seed with this, then follow `onChanged`. The store's
`entries` signal is this list attached to the strip.

The list is already cut to what this bar shows (app 0.3.11+). In
**Settings, Taskbar**, **Show taskbar apps on** offers the three Windows
modes: every bar lists every window (the default), each bar lists only
the windows on its own display, or the primary display's bar lists every
window and the others only their own. `windows.list` and `onChanged` both
follow that choice, so a bar needs no code for it.

### dd.windows.focus() [#ddwindowsfocus]

```ts
dd.windows.focus(id: string): Promise<void>
```

Restores the window if it is minimized, then asks Windows for the
foreground. Fire and forget: a refused foreground attempt still resolves
and is only logged host-side (the OS foreground lock; an elevated window,
where the host falls back to the Alt-Tab switch path, which works). Drive
your focus indicator from `onChanged`, never from the click, and a refusal
can never desync it.

### dd.windows.minimize() [#ddwindowsminimize]

```ts
dd.windows.minimize(id: string): Promise<void>
```

Minimizes the window; the indicator update arrives through `onChanged`
when focus moves. Windows refuses the request for an elevated window (Task
Manager, an elevated terminal) and the click then does nothing. That is
not a trap: such a window can only be minimized from its own caption, and
the next click on its button restores it through `focus`.

### dd.windows.icons() [#ddwindowsicons]

```ts
dd.windows.icons(keys: string[]): Promise<{ icons: Record<string, string> }>
```

Icon PNG data URIs (up to 96 px) for up to 64 `iconKey`s from the current
list, extracted on demand and cached per app for the session. Keys the
host cannot resolve are simply absent: keep a glyph fallback and do not ask
again (the miss is cached too). Icons are never inlined in the list or the
stream, so cache by `iconKey` and fetch only unseen keys; the store's
`icon(iconKey)` signal does exactly that, in batches.

### dd.windows.onChanged() [#ddwindowsonchanged]

```ts
dd.windows.onChanged(cb: (p: { windows: OsWindow[] }) => void): () => void
```

Change-driven full snapshot: a window opened or closed, focus moved, a
title changed, minimize state flipped, a window changed monitors. It is
cut to this bar's windows the same way `list()` is. Nothing fires at a
fixed rate and there is nothing to poll. This is the driver for
a taskbar's focus indicator and button row.

## dd.pins [#ddpins]

DeskDash's own pinned apps: one host-owned list, kept in the app config and
shared by every bar widget, so switching bars, reloading or reinstalling a
bar never changes it. Pins are made and removed through the host's
[app menu](#ddbarappmenu), never by a widget, and moved through
[dd.bar.strip.move](#ddbarstripmove). On the first run the host imports the
Windows taskbar's own pins once, read-only. Same gate as `dd.windows`.

### dd.pins.list() [#ddpinslist]

```ts
dd.pins.list(): Promise<{ pins: PinnedApp[] }>
```

The pins in pin order: the pinned rows of the strip, in strip order. A
pin's `appKey` is its `id`, stamped onto every window it claims before the
window list goes out, so a pin and its running windows share one key from
the first frame either exists.

### dd.pins.launch() [#ddpinslaunch]

```ts
dd.pins.launch(id: string): Promise<void>
```

Launches the pinned app through the host's own launcher for it (a packaged
app's AppsFolder entry, the app's Start menu shortcut, or its exe; nothing
crosses the bridge). The app opens on the display of the bar that called
it (app 0.3.11+): the launch carries a hint for that display, and when a
bar runs on every display, a new window of the app that opens elsewhere
within about 10 seconds moves there, keeping its size. Windows that stay
where they open: one the app already showed and raised in answer to the
launch, one on the display under the mouse (a tab the user tears off),
and one that covers its whole monitor (a fullscreen game).
The window it opens arrives already carrying this
pin's `appKey`, so it merges on the `dd.windows.onChanged` that announces
it. Always a new-instance request: correlate running windows through
`appKey` and decide yourself whether a running pin's click should focus
instead (the store does). Rejects `NOT_FOUND` for a stale id and for a pin
whose launcher and exe are both gone; a launch the OS refuses rejects
`INTERNAL`. Rate limited to 5 launches per 10 seconds.

### dd.pins.onChanged() [#ddpinsonchanged]

```ts
dd.pins.onChanged(cb: (p: { pins: PinnedApp[] }) => void): () => void
```

Change-driven full snapshot: fires only when the pin list itself changed,
a pin made or removed from the app menu, or the pinned order moved. A pin's
`appKey` is fixed, so launching or quitting an app never emits here;
`dd.windows.onChanged` carries that.

## dd.bar.flyout [#ddbarflyout]

The host-drawn preview flyout: window titles, live snapshot previews and a
close button per card, for one app group at a time. You decide when and for
which group; the host does everything else: where it goes, what it shows,
when it closes. Bar document only (a pane cannot open it). The store and
`<dd-apps>` drive it for you (`previews="click"` or `"hover"`); the methods
below are the surface underneath.

### dd.bar.flyout.open() [#ddbarflyoutopen]

```ts
dd.bar.flyout.open(opts: {
  key: string;             // your group tag, up to 64 chars; echoed on onChanged
  windowIds: string[];     // 1 to 32 ids from the current window list
  anchor: Element | BarAnchor;
  style?: BarFlyoutStyle;
}): Promise<void>
```

Opens the flyout for a group, or retargets it while open (the instant
switch, no flicker). Stale ids are pruned silently and a fully stale group
just closes. The flyout centers on the anchor beside the strip (above a
bottom bar, to the side of a left or right bar), clamped to the display,
up to 8 cards. `style` is the curated author styling: every knob defaults
to the active theme's tokens, and colors are hex only, never a CSS
expression.

Fire and forget: the content builds host-side, so drive your hover machine
from `onChanged`, never from this promise. A click on a card focuses its
window (restores it if minimized, never minimizes) and closes the flyout;
the card's X posts a graceful close request to the app, which may prompt
to save.

What a card shows. Previews are snapshots the host takes from the window,
refreshed about once a second while the flyout is open. A card falls back
to the app's icon tile when there is nothing to capture: a minimized
window, a window that draws straight to the GPU (games, some browsers and
players), or an elevated window, which refuses capture. None of that is
yours to fix; design the icon tile to look intended.

### dd.bar.flyout.close() [#ddbarflyoutclose]

```ts
dd.bar.flyout.close(): Promise<void>
```

Advisory close, meant for pointer-leave: the host holds a short grace
(about 300 ms, so the pointer can cross to the panel) and ignores it while
the cursor is inside the flyout itself, whose own leave then closes it. The
host also force-closes on its own: edit mode, a reload, a fullscreen app,
the app menu or a pane opening, the bar going away or crashing, an emptied
group, and a no-hover backstop of about 10 seconds. A stranded flyout is
never the author's problem.

### dd.bar.flyout.onChanged() [#ddbarflyoutonchanged]

```ts
dd.bar.flyout.onChanged(cb: (p: { open: boolean; key: string | null }) => void): () => void
```

The flyout's real state, on every open, switch and close, whoever caused
it. `key` echoes the tag from `open` and is `null` once closed. This is the
only truth for open state and the driver for switch-while-open hover
behavior; never assume the state from your own calls.

## dd.systray [#ddsystray]

The Windows notification area ("system tray"), mirrored by the host so a bar
can offer it back once DeskDash replaces the native taskbar. You get a count,
never the icons: the rows, and the clicks that reach those apps, live in a
host-drawn window that no widget can read or script.

What that list shows, so you can explain it to your users: every app's
tray icon, including the ones an app marked hidden (dimmed, because once
the native taskbar is gone this list is the only way to reach them), and
DeskDash's own icon; Windows' own volume, network and battery icons stay
with Windows and are not listed. Rows carry the app's tooltip or its
process name, sorted by name, twelve at a time with the rest scrolling.

The easiest way to use it is the [`<dd-tray-caret>`](/docs/reference/components#dd-tray-caret)
component with the `systray` setting preset; the methods below are the surface
underneath it.

### dd.systray.subscribe() [#ddsystraysubscribe]

```ts
dd.systray.subscribe(): Promise<{ count: number }>
```

Arms the host reader (the first call only, so a bar that never shows the tray
costs nothing) and returns the count of shown icons. Seed a caret's
visibility from the count, then follow `systray.changed`.

### dd.systray.open() [#ddsystrayopen]

```ts
dd.systray.open(anchor: Element | BarAnchor): Promise<void>
```

Opens the host-drawn tray list, anchored on your caret. Pass the element and
the SDK measures it. The host owns the list's size, position, dismissal and
every click inside it. Fire and forget: follow `systray.changed`, not this
promise.

### dd.systray.close() [#ddsystrayclose]

```ts
dd.systray.close(): Promise<void>
```

Advisory close (your caret toggled off). The host also closes on its own: an
outside press, Escape, a left click on a row, or the bar going away. A
right-click on a row keeps the list open so the app's own menu can sit over
it, the way the Windows tray overflow does.

### dd.systray.onChanged() [#ddsystrayonchanged]

```ts
dd.systray.onChanged(cb: (p: { count: number; open: boolean }) => void): () => void
```

Fires on every tray change and every open or close of the list, whoever
caused it. This is the correct driver for a caret's pressed look: the host
closes the list on its own, so follow this, never your own guess. From app
0.4.2, `open` is true only on the bar whose caret opened the list, and the
list also closes when the bar ducks for a fullscreen app, in edit mode, and
when the bar goes away.

## dd.notificationCenter [#ddnotificationcenter]

The Windows notification center, drawn by the host the way the tray list
is, so a bar can offer it once DeskDash replaces the native taskbar. You get
a count and states, never the notifications: their text, pictures and
buttons, and every click on them, live in a host-drawn window that no
widget can read or script. That is also why it needs no permission of its
own beyond `taskbar`, and why a bar can show notifications next to anything
else it does, weather included. App 0.4.2 or newer.

What the window shows, so you can explain it to your users: other apps'
notifications, newest first, grouped by app with the app's icon and a clear
button, each with its sender picture, title, time, text and pictures, and
its buttons (reply boxes, snooze and right-click entries are left out). A
click on one does what a click in Windows does: the app opens, or jumps to
the message when it registered for that, and the window closes. Dismiss,
clear and Clear all remove notifications from Windows too. When Windows
refuses access (its own privacy switch for notifications), the window says
so and links to the settings page, with a way to open Windows' own center
instead.

The easiest way to use it is the
[`<dd-notification-bell>`](/docs/reference/components#dd-notification-bell)
component, optionally with the `notificationCenter` setting preset; the
methods below are the surface underneath it.

### dd.notificationCenter.subscribe() [#ddnotificationcentersubscribe]

```ts
dd.notificationCenter.subscribe(): Promise<{
  count: number;                               // notifications in Windows now
  open: boolean;                               // the window is open from THIS bar
  dnd: "off" | "priority" | "alarms" | null;   // Windows' Do not disturb mode
  access: "allowed" | "denied" | "unspecified" | "unavailable";
}>
```

Arms the host reader (the first call only; it lists Windows' notifications
every second from then on, so a bar without a bell reads nothing) and
returns the state, fresh. `access` is `"denied"` when the user's privacy
switch is off, `"unspecified"` when Windows has not decided, `"unavailable"`
when the Windows API failed; the window explains each, so a bell can simply
open it.

### dd.notificationCenter.open() [#ddnotificationcenteropen]

```ts
dd.notificationCenter.open(anchor: Element | BarAnchor): Promise<void>
```

Opens the host's notification center beside your bell. The host reads
Windows first, then owns the window's size (its content, up to the room
beside the bar), position, dismissal and every click inside it. Fire and
forget: follow `notificationCenter.changed`, not this promise.

### dd.notificationCenter.close() [#ddnotificationcenterclose]

```ts
dd.notificationCenter.close(): Promise<void>
```

Advisory close (your bell toggled off). The host also closes on its own: an
outside press, Escape, a click that opened an app, the bar ducking for a
fullscreen app, edit mode, or the bar going away.

### dd.notificationCenter.onChanged() [#ddnotificationcenteronchanged]

```ts
dd.notificationCenter.onChanged(
  cb: (state: { count: number; open: boolean; dnd: string | null; access: string }) => void,
): () => void
```

Fires on every change of the count, the access or the mode, and every open
or close of the window, whoever caused it. The correct driver for a bell's
pressed look and badge.

## dd.bar.openStartMenu [#ddbaropenstartmenu]

```ts
dd.bar.openStartMenu(): Promise<void>
```

Opens the Windows start menu (the host taps the Windows key). A second call
while it is open toggles it closed, the native behavior. There is no
documented API for the menu itself, so the tap is a no-op on the secure
desktop or while a held modifier turns it into a chord. Fire and forget:
there is no menu state to follow.

## dd.bar.openQuickSettings [#ddbaropenquicksettings]

```ts
dd.bar.openQuickSettings(): Promise<void>
```

Opens Windows' own Quick Settings: Wi-Fi, Bluetooth, the sound output
device, Night light and every other toggle a hidden taskbar takes away (the
host sends Win+A). A second call while it is open toggles it closed.
Windows places it in the bottom right corner, not beside your button. Call
it from a click: the host refuses it in edit mode and while the cursor is
off the bar window, and logs the refusal. Fire and forget: the host cannot
see when the menu closes. 5 calls per rolling 10 seconds. App 0.4.2 or
newer.

## dd.bar.openNotifications [#ddbaropennotifications]

```ts
dd.bar.openNotifications(): Promise<void>
```

Opens Windows' notification center with its calendar (the host sends
Win+N), with the same placement, click rule and rate limit as
`dd.bar.openQuickSettings`. App 0.4.2 or newer. For a notification center
beside your own bar, use [dd.notificationCenter](#ddnotificationcenter)
instead.

## dd.bar.appMenu [#ddbarappmenu]

```ts
dd.bar.appMenu(opts: {
  appKey?: string;    // exactly one of the three
  windowId?: string;
  pinId?: string;
  anchor: Element | BarAnchor;
}): Promise<void>
```

Opens the host-drawn per-app context menu above the bar for one subject:
a merged group by its `appKey`, a single window by its id (for a window
without an appKey), or a pin by its id (for a pin without one). The menu
shows the app's name (launches a new instance), **Pin to taskbar** or
**Unpin from taskbar**, and **Close window** (or **Close all windows** for
a group with several). Pins live in the DeskDash config, one list for
every bar widget (switching bars never resets it), and every change comes
back through `dd.bar.strip.onChanged` and `dd.pins.onChanged`. Pin and
unpin happen in place on the strip: the app keeps its slot.

A pin's `appKey` is its own id, the id of its strip row, and the host stamps
that same token onto every window the pin claims before it hands you the
window list. So a pin and its running windows always share one key, from
the first frame either exists, and a group key, a pin id and a strip row id
are the same token.

You rarely call this yourself. `<dd-app>` tags itself; by hand, tag each
app button with the subject and the SDK's right-click handler does it for
you:

```html
<button data-dd-app="3f9a1c2e">...</button>
<button data-dd-window="131586">...</button>
<button data-dd-pin="9c2f0b4e1d7a6f35">...</button>
```

Re-stamp the tag on every render (ids move as windows come and go). An
element whose own `contextmenu` handler calls `preventDefault()` before the
SDK's handler keeps the right-click; untagged space raises
[the bar's own menu](#the-bars-own-menu).

Trust model: you only say which app was right-clicked. Launch, pin, unpin
and close run host-side from clicks inside the host's own menu window, so
the method grants a widget nothing beyond opening the menu, and the
`taskbar` permission stays a read grant. Honored only while the cursor is
over the bar and never in edit mode; the host closes the menu on Escape, an
outside click, an action, or when the subject disappears. Fire and forget.

## The bar's own menu [#the-bars-own-menu]

A right-click on untagged space in the bar document raises the host's
native menu for the bar itself: &#x2A;*Change taskbar...** (the marketplace's
Taskbars section, the one place a bar is swapped), &#x2A;*Taskbar settings...**
(the bar's own settings dialog, with the Placement group first),
&#x2A;*Settings...**, **Edit layout** and **Quit DeskDash**. Quit lives here on
purpose: while the native taskbar is replaced, the notification area is
hidden with it, so the bar's menu is the one always-reachable way out, and
a clean quit is also what brings the Windows taskbar back. The SDK forwards
the right-click and suppresses the WebView's default menu; the request
underneath (`bar.menu`) is bar-window-only and you never need to call it.


# Core runtime (/docs/reference/api/core)



Every widget document gets one global, `dd`. The SDK script creates it:

```html
<script src="/__sdk/widget-sdk.js"></script>
```

On load the SDK connects your document to the host, applies the design tokens
to `:root`, and resolves `dd.ready` once the host has answered. Everything a
widget can do (read settings, fetch data, stream system state) goes through
`dd`; there is no other door to the system.

## dd.ready [#ddready]

The init context, resolved once per document.

```ts
dd.ready: Promise<DdContext>

interface DdContext {
  /** Per instance; a taskbar document (and its pane) carries the
   *  sentinel "__bar" on every display. */
  instanceId: string;
  /** "main" = the widget document; "pane" = a popout pane document;
   *  "bar" = the taskbar surface. Same grants either way. */
  role: "main" | "pane" | "bar";
  widget: { id: string; version: string };
  /** What the user granted: every permission key, plus http.hosts and
   *  links.prefixes. Mirrors the manifest after install-time consent. */
  grants: DdGrants;
  /** The manifest's interactive flag. Always false for panes. */
  interactive: boolean;
  /** Manifest defaults merged with the user's stored values. */
  settings: Record<string, unknown>;
  /** The current --dd-* token map. The SDK applies it for you. */
  tokens: Record<string, string>;
  /** Bridge API info: feature-detect against version; deprecated maps a
   *  method name to its deprecation note. */
  api?: { version: number; minSupported: number; deprecated: Record<string, string> };
  /** Static for the whole app run; absent on older hosts (treat as off). */
  perf?: { lowSpec: boolean };
  /** Pane documents only: the applied side and the data open() passed. */
  popout?: { side: "down" | "up" | "left" | "right"; data: unknown };
  /** Bar documents (and a bar's pane): the screen edge the bar window
   *  docks to. Absent on hosts older than this field: treat as "bottom". */
  bar?: { edge: "bottom" | "left" | "right" };
}
```

Notes:

* `perf.lowSpec` is true when the user enabled low performance mode. It never
  changes mid-run, so read it once. Widgets that animate should cap their draw
  loop around 30 fps and their canvas backing store at 1x `devicePixelRatio`;
  the host already lowers its own side (spectrum frames arrive at 20 Hz
  instead of 30).
* A pane document (see
  [popouts](/docs/reference/api/widgets-together)) boots the SDK exactly like
  the main document and carries the same instance identity and grants.
* A bar document (see [bar widgets](/docs/reference/api/bar-widgets) and
  [building a taskbar](/docs/guides/taskbars)) has `role: "bar"`, real
  mouse input until it reports segments, no grid-style popouts (its pane
  opens beside the strip) and `bar.edge` set; `interactive` is always
  false for it.

## dd.request() [#ddrequest]

The low-level escape hatch. Every namespaced method wraps it; call it directly
only for something the typed surface does not cover.

```ts
dd.request<T>(method: string, params?: unknown, timeoutMs?: number): Promise<T>
```

Requests reject with an `Error` carrying a `code`:

```ts
interface DdError extends Error {
  code: string; // "PERMISSION_DENIED", "RATE_LIMITED", ...
}
```

The full code list is on
[Errors and versioning](/docs/reference/errors-versioning). The client rejects
with `TIMEOUT` after 15 seconds by default; `timeoutMs: 0` disables the client
timer (the dialog pickers use this, since they wait on the user).

## dd.poll() [#ddpoll]

The polling loop widgets keep re-implementing: an interval, error backoff,
staleness so a settings-driven refresh cancels the in-flight cycle, and the
settings trigger itself.

```ts
dd.poll<T>(opts: PollOptions<T>): PollHandle

interface PollOptions<T> {
  /** Do the work; return the data (or throw to trigger onError + backoff). */
  fetch: () => Promise<T>;
  onData: (data: T) => void;
  onError?: (err: unknown) => void;
  /** Milliseconds until the next poll after a success: a number, or a getter
   *  read each cycle so it can follow a live setting. */
  every: number | (() => number);
  /** Exponential backoff from 1s on error, capped at every (default true). */
  backoff?: boolean;
  /** Setting keys whose VALUE changing should re-poll, coalesced over
   *  refreshDebounceMs. Keys left out never cost a request. */
  refreshOn?: string[];
  /** How long the watched keys must rest before the re-poll (default 400ms). */
  refreshDebounceMs?: number;
}

interface PollHandle {
  /** Cancel any in-flight or scheduled cycle and poll now. */
  refresh: () => void;
  stop: () => void;
}
```

Polling starts immediately. The usual wiring, with `refreshOn` naming the
settings the fetch actually reads:

```js
dd.settings.bind((next) => {
  settings = next;          // repaint on every change
});

dd.poll({
  fetch: () => fetchForecast(settings),
  onData: render,
  every: () => Number(settings.refreshMinutes) * 60_000,
  refreshOn: ["city", "units", "refreshMinutes"],
});
```

**Do not call `refresh()` from a settings callback.** The dialog live-previews
your settings as the user types, one change per keystroke, so that version
re-fetches once per CHARACTER: a nine-letter city name is nine requests, and
`dd.http.fetch` allows 30 a minute. `refreshOn` coalesces the burst into one
poll once the values rest, and a setting you leave out of the list, a panel
restyle for instance, costs nothing at all.

`refresh()` stays for the cases it suits: a manual reload button, or a retry
after the user fixes something.

## dd.log() [#ddlog]

```ts
dd.log(level: "debug" | "info" | "warn" | "error", ...args: unknown[]): void
```

Writes to the host's log, tagged with your widget and instance id. Uncaught
errors and unhandled rejections in your document are forwarded there
automatically, so the host log is the first place to look when a widget
misbehaves.

The host sanitizes forwarded text before writing it: URL query strings are
replaced with `?...`, home-directory paths collapse to `~`, and each line is
capped at 8 KB. Users share their log file for support, so nothing a widget
logs should be able to leak a token or a username. Log identifiers and state,
not raw URLs or file paths.

## dd.links.open() [#ddlinksopen]

The one way out of the widget: open something with the OS default handler.

```ts
dd.links.open(url: string): Promise<void>
```

Needs the `links` permission, and `url` must start with one of the manifest's
`links.prefixes` entries (each a full `scheme://...` string, like
`steam://run/` or `https://github.com/`). Dangerous schemes (`file`,
`javascript`, `data`, and friends) are refused regardless of what a manifest
declares. Honored only while the real cursor is inside the widget (call it
from a click handler; for a bar, the bar window or its pane, app 0.3.7 or
newer) and never in edit mode. 5 calls per rolling 10 seconds.

A feed reader needs no `links` grant for its headlines:
[dd.feeds.open](/docs/reference/api/data-services#ddfeedsopen) opens the item
links of the user's own feed.

## How permission gating works [#how-permission-gating-works]

The host checks permissions before a method dispatches. Most methods map to
the permission named by their prefix:

| Prefix                              | Needs                                                      |
| ----------------------------------- | ---------------------------------------------------------- |
| `settings.*`, `theme.*`, `widget.*` | nothing, always available                                  |
| `time.*`                            | `time`                                                     |
| `storage.*`                         | `storage`                                                  |
| `files.*`                           | `files`                                                    |
| `notify.*`                          | `notifications`                                            |
| `http.*`                            | `http`, with the exact host allowlisted                    |
| `links.*`                           | `links`, with a matching prefix                            |
| `desktop.*`                         | `desktop`                                                  |
| `folders.*`                         | `folders`                                                  |
| `apps.*`                            | `systemApps`                                               |
| `games.*`                           | `games`                                                    |
| `launchers.*`                       | `launchers`                                                |
| `feeds.*`                           | `feeds`                                                    |
| `system.*` reads                    | `system`                                                   |
| `system.*` writes                   | `system` + `systemControls`                                |
| `media.*` reads                     | `media`                                                    |
| `media.*` controls                  | `media` + `mediaControls`                                  |
| `audio.*`                           | `audio`                                                    |
| `widgets.*`                         | `control`                                                  |
| `peers.*`                           | `peers`                                                    |
| `claudeUsage.*`                     | `claudeUsage`                                              |
| `power.*`                           | `power`                                                    |
| `dialogs.pickDesktopItems`          | `desktop`                                                  |
| `dialogs.pickFolderItems`           | `folders`                                                  |
| `popout.*`                          | a manifest `popout` block (a capability, not a permission) |
| `input.*`                           | the manifest `interactive` flag (SDK-managed)              |

A call without its grant rejects `PERMISSION_DENIED`. See the
[permissions reference](/docs/reference/permissions) for what each grant
means to the user.

Rules that apply everywhere:

* Every `on*` subscription returns an unsubscribe function.
* Rate limits are per instance over a rolling 10 second window unless a
  method says otherwise; exceeding one rejects `RATE_LIMITED`.
* Serialized request params are capped at 256 KB.

## How the connection works [#how-the-connection-works]

Widget documents run in sandboxed iframes with an opaque origin. On load the
SDK performs a small handshake with the host and receives a dedicated message
port plus the init context; all further traffic flows over that port, and the
host ties every message to the frame it physically arrived from. None of this
is your concern day to day: the `dd` namespaces are the whole surface, and
nothing a widget puts inside a message can change which widget the host
believes it is talking to.


# Data services (/docs/reference/api/data-services)



## dd.http [#ddhttp]

Needs the `http` permission with the exact hostname allowlisted in the
manifest (`permissions.http.hosts`). All traffic goes through the host's
proxy; widgets have no direct network access at all.

### dd.http.fetch() [#ddhttpfetch]

```ts
dd.http.fetch(req: {
  url: string;
  method?: string;   // GET (default), POST, PUT, PATCH, DELETE, HEAD
  headers?: Record<string, string | { $secret: string }>;
  /** Extra query params merged into the URL host-side after allowlist
   *  validation. Values may be { $secret: name } like headers. */
  query?: Record<string, string | { $secret: string }>;
  body?: string;
  /** "dataUrl" returns the raw response bytes as a base64 data: URL
   *  (bodyText/json omitted), for images. Default "text". */
  as?: "text" | "dataUrl";
}): Promise<{
  status: number;
  headers: Record<string, string>;
  bodyText?: string;
  json?: unknown;    // parsed when the response is JSON
  dataUrl?: string;
}>
```

The rules, all enforced host-side:

* **Hosts.** The URL's hostname must exactly match an entry in
  `permissions.http.hosts`. `https` only (`http` allowed for localhost).
  Redirects are followed up to 4 hops, each hop re-validated against the
  allowlist.
* **Methods.** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`. Headers that
  would let a widget impersonate the browser session (`host`, `cookie`,
  `origin`, `referer`, connection-control headers) are refused; the response
  exposes a small safe set (`content-type`, `content-length`, `date`,
  `cache-control`, `etag`, `last-modified`).
* **Secrets.** A header or query value of `{ "$secret": "settingKey" }` names
  one of this widget's `secretRef` settings; the host injects the user's
  stored value after validation. The resolved value (and the merged URL)
  never appears in widget JS, logs, or error messages, and secret query
  params are never re-appended across redirects.
* **Sizes and pacing.** Request body up to 256 KB; response up to 1 MB;
  query up to 32 pairs (keys 128 bytes, values 2048 bytes, final URL 8 KB);
  30 second timeout; 2 concurrent requests per instance; 30 requests per
  minute by default, raised by the manifest's `http.rateLimit` up to 120.
* **Binary.** `as: "dataUrl"` returns the bytes as a base64 `data:` URL
  (same 1 MB cap). The widget CSP allows `img-src data:`, so the result can
  feed an `img` or `dd-icon` directly.

```js
const res = await dd.http.fetch({
  url: "https://api.example.com/v1/status",
  headers: { authorization: { $secret: "apiKey" } },
});
if (res.status === 200) render(res.json);
```

## dd.feeds [#ddfeeds]

Needs the `feeds` permission (app 0.3.9+, and 0.3.11+ for
`dd.feeds.open`; the store sets `minAppVersion`). Reads a news feed from the web address the user typed into
one of the widget's `url` settings, and opens its items in the browser. The
host fetches and parses the feed and hands back only the parsed items, never
the response, so the grant cannot fetch anything else.

### dd.feeds.fetch() [#ddfeedsfetch]

```ts
dd.feeds.fetch(opts: { settingKey: string }): Promise<FeedResult>

interface FeedResult {
  /** The feed's own title (at most 120 characters), or null. */
  title: string | null;
  /** Up to 50 items, in the order the feed lists them. */
  items: FeedItem[];
  /** When the host last downloaded this feed (epoch ms). */
  fetchedAtMs: number;
}

interface FeedItem {
  /** Plain text on one line: tags removed, entities decoded, at most 300
   *  characters. Items without a title never arrive. */
  title: string;
  /** An absolute http(s) address of at most 1024 bytes, or null. Open it
   *  with dd.feeds.open. */
  link: string | null;
  /** The published time, else the updated time, in epoch ms; or null. */
  publishedMs: number | null;
}
```

`settingKey` names one of this widget's `url` settings. A `url` field never
inherits its manifest default, so the widget shows a setup state until the
user types an address; put an example in the field's `placeholder`. RSS 0.9,
1.0 and 2.0, Atom and JSON Feed all work.

The rules, all enforced host-side:

* **Address.** `https` only (`http` allowed for localhost). Redirects are
  followed up to 4 hops, each held to the same rule, and a hop from a public
  address into loopback or a private network is refused.
* **Cache.** One cache per address, shared by every widget that reads it. A
  call within 5 minutes of a download answers from memory; a later one
  revalidates with the ETag and Last-Modified the server sent.
* **Output.** Only titles, links and dates cross the bridge. Put titles on
  screen with `textContent`, and open links with
  [dd.feeds.open](#ddfeedsopen), which needs no `links` grant.
* **Errors.** `NOT_FOUND` while the setting is blank or when the feed is gone
  (404 or 410); `INVALID_PARAMS` for an address that is not https;
  `PERMISSION_DENIED` when `settingKey` is not a `url` field; `INTERNAL` when
  the page is not a feed or the server fails. No error carries the address.
* **Sizes and pacing.** Response up to 1 MB, 30 second timeout; 10 calls per
  rolling 10 seconds.

```js
dd.poll({
  every: 15 * 60_000,
  refreshOn: ["feedUrl"],
  fetch: () => dd.feeds.fetch({ settingKey: "feedUrl" }),
  onData: (feed) => render(feed.items),
  onError: (err) => (err.code === "NOT_FOUND" ? showSetup() : showStale()),
});
```

### dd.feeds.open() [#ddfeedsopen]

```ts
dd.feeds.open(opts: { settingKey: string; link: string }): Promise<void>
```

App 0.3.11+. Opens an item `link` from
`dd.feeds.fetch` in the default browser. The widget needs no `links` grant:
the host opens only a link it parsed out of the feed at the address the user
typed into that same `url` setting, so a widget can never pick another page.

* **Which links.** The items of the newest answer for that address, plus
  those of earlier answers, up to 200 links. A headline that is still on
  screen after a refresh still opens. When the host no longer holds the
  address in its cache, the call reads the feed once before it decides.
* **Gesture.** Honored only while the real cursor is inside the widget, and
  never in edit mode. Call it from a click handler.
* **Errors.** `NOT_FOUND` when the link is in none of those answers (the
  headline left the feed); `PERMISSION_DENIED` outside a click; plus the
  `dd.feeds.fetch` errors for the setting.
* **Pacing.** 5 calls per rolling 10 seconds.

```js
strip.addEventListener("click", (event) => {
  const item = event.target.closest("[data-link]");
  if (!item) return;
  dd.feeds
    .open({ settingKey: "feedUrl", link: item.dataset.link })
    .catch((err) => dd.log("warn", "headline open failed", err.code));
});
```

## dd.weather [#ddweather]

The shared weather stack, built on Open-Meteo. Convenience code, not a
capability: everything routes through this widget's own grants. `geocode`
needs `geocoding-api.open-meteo.com` in `http.hosts`, `current` and
`forecast` need `api.open-meteo.com`, and the geocode cache uses your
`storage` grant.

### dd.weather.describe() [#ddweatherdescribe]

```ts
dd.weather.describe(code: number, isDay: number | boolean): {
  label: string;
  glyph: WeatherGlyph;
}

type WeatherGlyph =
  | "clearDay" | "clearNight" | "partlyDay" | "partlyNight" | "cloudy"
  | "fog" | "drizzle" | "rain" | "snow" | "thunder";
```

Maps a WMO weather code to a human label plus a glyph name for `icon()`.

### dd.weather.geocode() [#ddweathergeocode]

```ts
dd.weather.geocode(query: string, opts?: { cacheKey?: string }): Promise<{
  query: string;
  lat: number;
  lon: number;
  label: string;
}>
```

Resolves a place name, cached in the widget's own storage (one entry per
`cacheKey`; a changed query re-fetches).

### dd.weather.current() [#ddweathercurrent]

```ts
dd.weather.current(opts: {
  lat: number;
  lon: number;
  units?: "celsius" | "fahrenheit";
}): Promise<{
  temperature: number;  // in the requested unit
  code: number;         // WMO code; feed it to describe()
  isDay: boolean;
}>
```

### dd.weather.forecast() [#ddweatherforecast]

```ts
dd.weather.forecast(opts: {
  lat: number;
  lon: number;
  units?: "celsius" | "fahrenheit";
  scale: "hourly" | "daily";
  count?: number;       // slots to return, 1..24 (default 5)
  stepHours?: number;   // hourly only: hours between slots, 1..12 (default 1)
}): Promise<Array<{
  time: string;         // local ISO: "2026-08-28T15:00" hourly, "2026-08-29" daily
  code: number;         // WMO code; feed it to describe()
  isDay: boolean;       // daily slots are always true
  temperature: number;  // hourly: the temperature. daily: the high
  low?: number;         // daily only
}>>
```

The next slots of the forecast, hour by hour or day by day. **Slot 0 is always
the current period**, this hour or today, so you can label it "Now" or "Today"
without a second call; `stepHours` spaces the hourly slots, which lets five of
them reach a day and a half ahead.

Unlike `geocode`, this is stateless: nothing is cached for you. Keep your own
last-good snapshot in `dd.storage` if you want the widget to paint something
after a remount or while the network is down.

The array is short rather than padded when the forecast window ends, and a
malformed or empty response throws instead of returning partial slots.

### dd.weather.icon() [#ddweathericon]

```ts
dd.weather.icon(glyph: WeatherGlyph | string): string
```

A built-in line-art icon as a `data:` URI, ready for `<dd-icon src=...>`
(the widget CSP allows `img-src data:`). Unknown names return `""`.

## dd.claudeUsage [#ddclaudeusage]

Needs the `claudeUsage` permission.

### dd.claudeUsage.status() [#ddclaudeusagestatus]

```ts
dd.claudeUsage.status(opts: { dirKey: string }): Promise<ClaudeUsage>
```

Claude Code's own usage as one aggregated snapshot. `dirKey` names one of
this widget's `filePath` settings; the host resolves the `.claude` directory
the user configured there (blank = the default install), reads the limit
bars, today's tokens by model, and recently-active sessions, and refreshes
the bars from Anthropic's usage endpoint in the background. The OAuth token,
transcript contents, and raw paths never reach the widget. Poll it. 20 calls
per rolling 10 seconds.

```ts
interface ClaudeUsage {
  /** False when the configured directory is not a readable Claude home:
   *  show a "set Claude home dir" hint. */
  dirOk: boolean;
  claudeVersion: string | null;
  /** Where limits came from: "live" (Anthropic's usage endpoint), "cache"
   *  (Claude Code's own cache), or "none" (nothing available yet). */
  source: "live" | "cache" | "none";
  /** When limits was produced (epoch ms), for an "as of" readout. */
  fetchedAtMs: number | null;
  /** One bar per active limit window; percent is 0..100. */
  limits: {
    kind: string;
    group: string;
    label: string;
    percent: number;
    severity: string;
    resetsAtMs: number | null;
    isActive: boolean;
  }[];
  /** Today's usage summed from the transcripts (local day), by model. */
  today: {
    byModel: {
      model: string;
      input: number;
      output: number;
      cacheRead: number;
      cacheWrite: number;
      messages: number;
    }[];
    totals: {
      input: number;
      output: number;
      cacheRead: number;
      cacheWrite: number;
      messages: number;
    };
  };
  /** Recently-active Claude Code sessions. */
  sessions: {
    name: string | null;
    cwd: string;
    status: string;
    updatedAtMs: number;
  }[];
}
```


# Desktop, folders, apps, games, and launchers (/docs/reference/api/desktop-apps)



Four namespaces share one design: the host enumerates, the widget gets
opaque ids, and opening something means handing an id back. A widget never
passes a path, so it can only ever open what the host really listed.

They also share the change-ping pattern: `onChange` events carry no data,
they just tell you to list again.

## dd.desktop [#dddesktop]

Needs the `desktop` permission.

### dd.desktop.list() [#dddesktoplist]

```ts
dd.desktop.list(): Promise<{ items: DesktopItem[] }>

interface DesktopItem {
  /** Opaque host-issued id: the only handle open() accepts. */
  id: string;
  name: string;
  kind: "file" | "folder" | "shortcut" | "virtual";
  /** Filesystem path; null for virtual items (Recycle Bin, This PC, ...). */
  path: string | null;
  /** Icon PNG data URI (up to 96 px), or null when the host couldn't extract one. */
  icon: string | null;
}
```

Everything currently on the user's desktop (user and public folders plus
visible virtual items), sorted by name. Served from a host cache the desktop
watcher keeps fresh, so it is cheap to call.

### dd.desktop.open() [#dddesktopopen]

```ts
dd.desktop.open(id: string): Promise<void>
```

Opens the item like a desktop double-click; a folder item opens in the file
manager the user chose in Settings > General, when one is set (app 0.3.7 or
newer). Accepts only ids from `list`; a
stale id (the item disappeared since your last list) rejects `NOT_FOUND`, so
re-list and retry. 5 opens per rolling 10 seconds, shared with
`folders.open`.

### dd.desktop.onChange() [#dddesktoponchange]

```ts
dd.desktop.onChange(cb: () => void): () => void
```

Data-free ping when the desktop's item set changes (add, remove, rename,
icon refresh): call `list` again. Changes are debounced around 400 ms.

## dd.folders [#ddfolders]

Needs the `folders` permission. All methods take `settingKey`, the name of
one of this widget's `filePath` settings; the host resolves the folder the
user pointed it at. A blank setting value resolves to the desktop source,
which additionally requires the `desktop` grant.

### dd.folders.list() [#ddfolderslist]

```ts
dd.folders.list(settingKey: string): Promise<{
  items: DesktopItem[];
  total: number;
  truncated: boolean;
  error?: string;
}>
```

One non-recursive listing of the granted folder. Items arrive with
`icon: null` here; fetch icons for what you actually render via `icons` (the
desktop source inlines them instead). Listings cap at 500 items
(`truncated: true`, `total` is the real count). An unreadable folder
(unplugged drive, dead share) answers the last good items plus
`error: "unavailable"` rather than rejecting. 30 calls per rolling 10
seconds.

### dd.folders.icons() [#ddfoldersicons]

```ts
dd.folders.icons(settingKey: string, ids: string[]): Promise<{
  icons: Record<string, string>;
}>
```

Icon PNG data URIs (up to 96 px) for up to 128 listed ids, extracted on demand and cached
host-side. Unknown or extraction-failed ids are simply absent from the
result, so keep a glyph fallback. 60 calls per rolling 10 seconds.

### dd.folders.open() [#ddfoldersopen]

```ts
dd.folders.open(settingKey: string, id: string): Promise<void>
```

Opens a listed item like a double-click in Explorer; a folder item opens in
the file manager the user chose in Settings > General, when one is set (app
0.3.7 or newer). Stale ids reject
`NOT_FOUND` (re-list). Shares `desktop.open`'s 5 per 10 seconds launch
bucket, whichever source the id lives in.

### dd.folders.onChange() [#ddfoldersonchange]

```ts
dd.folders.onChange(cb: () => void): () => void
```

Data-free ping when a folder one of your `filePath` settings points at
changes (the desktop, for blank settings): re-list. Delivered only to the
instances whose folder actually changed. Folders that cannot be watched
(network shares generally) re-enumerate on every list instead of pretending.

## dd.apps [#ddapps]

Needs the `systemApps` permission.

### dd.apps.list() [#ddappslist]

```ts
dd.apps.list(): Promise<{ apps: SystemApp[] }>

interface SystemApp {
  /** Catalog key: the only handle launch() accepts. */
  id: string;
  name: string;
  /** Icon PNG data URI (up to 96 px), or null (render your own glyph). */
  icon: string | null;
}
```

The host's fixed catalog of Windows system apps (Settings, File Explorer,
Recycle Bin, Task Manager, Terminal, Control Panel, Calculator, Notepad,
Snipping Tool), with real icons where the host could extract them. Icons are
cached host-side after the first call. 30 calls per rolling 10 seconds.

### dd.apps.launch() [#ddappslaunch]

```ts
dd.apps.launch(id: string): Promise<void>
```

Launches a catalog app by key. There is no path, URI, or executable surface
here; ids from `list` are the whole vocabulary. A shell-folder entry (the
Recycle Bin) opens in the file manager the user chose in Settings > General,
when one is set, handed the folder's shell id; the File Explorer entry always
launches explorer.exe (app 0.3.7 or newer). Each app carries host-owned
fallback launch targets; if all fail the call rejects `INTERNAL` naming the
app, and unknown ids reject `NOT_FOUND`. 5 calls per rolling 10 seconds.

## dd.games [#ddgames]

Needs the `games` permission. App 0.3.2 and newer.

### dd.games.list() [#ddgameslist]

```ts
dd.games.list(): Promise<{ games: InstalledGame[]; total: number }>

interface InstalledGame {
  /** Opaque host-issued id, the only handle launch and art accept. */
  id: string;
  name: string;
  store: "steam" | "gog";
  /** Tagged host-side, because a library is often a third demos. */
  kind: "game" | "demo" | "playtest";
  /** Epoch ms, or null when the store records no play history. */
  lastPlayedMs: number | null;
  /** Total minutes played, or null when the store records none. */
  playtimeMin: number | null;
  sizeBytes: number | null;
  installedMs: number | null;
}
```

Every game installed on this PC that a supported launcher vouches for, most
recently played first and then alphabetical.

The sources are the launchers' own records, read off disk and the registry:
Steam's per-game manifests plus its local play-time config, and the GOG
registry keys that both Galaxy and the offline installers write. There is no
folder scan anywhere in this, by design, so nothing else the user keeps on a
games drive can ever appear here.

The appid, the registry id and the install path never cross the bridge. A
widget sees the fields above and nothing more. GOG records no play history
that the host can read, so its games always report `lastPlayedMs: null` and
sort below anything played.

The scan is cached host-side for 60 seconds, so calling this every time a
pane opens is cheap. 20 calls per rolling 10 seconds.

### dd.games.art() [#ddgamesart]

```ts
dd.games.art(
  ids: string[],
  kind?: "portrait" | "header",
): Promise<{ art: Record<string, string> }>
```

Cover art for up to 32 listed ids, as PNG or JPEG data URIs.

It comes from the launcher's own on-disk cache, never the network. `kind`
picks the shape you lay out for: `portrait` (the default, a 2:3 library
capsule) or `header` (the wide banner). Whichever you ask for, the other
fills in when a title cached only that one, so a tile is rarely empty.

Ids with no cached art are simply absent from the result, so keep a
placeholder. A GOG entry answers its executable's icon, since GOG caches no
art. 30 calls per rolling 10 seconds.

### dd.games.launch() [#ddgameslaunch]

```ts
dd.games.launch(id: string): Promise<void>
```

Starts a listed game through its own launcher, by the target the host holds
for that id. As with `apps.launch`, ids are the whole vocabulary: there is no
path or command-line surface. An id from a stale list rejects `NOT_FOUND`
rather than starting anything. 5 calls per rolling 10 seconds.

## dd.launchers [#ddlaunchers]

Needs the `launchers` permission. Grid widgets only. App 0.3.9 and newer.

Drop-to-launch: the user drags files, folders, shortcuts or exes from
Explorer or the desktop onto your widget, and the host turns each one into a
launch item. The drop is the consent. Nothing prompts, and nothing your
widget says can add an item.

The store is per widget id, not per instance: any instance of your widget
may open any item dropped on any instance of it. Keep your own membership
list per instance (an id list in `dd.storage`), the way the drawer keeps
`members`. Ids not in the store stay latent, never wrong.

No path ever crosses the bridge. A widget sees the fields below and nothing
more; the host resolves and opens each item itself.

### dd.launchers.list() [#ddlauncherslist]

```ts
dd.launchers.list(): Promise<{ items: LauncherItem[] }>

interface LauncherItem {
  /** Opaque host-issued id, the only handle open and forget accept. */
  id: string;
  /** Frozen at drop time. */
  name: string;
  /** app = an exe or a shortcut to one; file and folder open like a double-click. */
  kind: "app" | "file" | "folder";
  /** Always null here: fetch via icons. */
  icon: string | null;
}
```

Everything the user dropped on your widget type, in drop order. An exe or a
shortcut resolves through the same ladder a taskbar pin uses, so its id
equals the pin of the same app; a file or folder hashes its full name, so a
desktop file keeps the id `dd.desktop.list` gives it. The same target
dropped twice dedupes.

Cap 256 items per widget; further drops are refused with a log line. 30
calls per rolling 10 seconds.

### dd.launchers.icons() [#ddlaunchersicons]

```ts
dd.launchers.icons(ids: string[]): Promise<{ icons: Record<string, string> }>
```

PNG data URIs (up to 96 px) for up to 128 ids of your store, extracted at
drop time and cached host-side for the session. Unknown or failed ids are
simply absent, so keep a glyph fallback. 60 calls per rolling 10 seconds.

### dd.launchers.open() [#ddlaunchersopen]

```ts
dd.launchers.open(id: string): Promise<void>
```

Opens an item like a double-click: an app through the shortcut or exe it was
dropped as (a dropped shortcut keeps its arguments), a file by name, a folder
in the file manager the user chose in Settings, when one is set. Accepts only
ids in your widget's store. A target that is gone rejects with `NOT_FOUND`.
No gesture gate: the user consented per item by dropping it. Shares the
desktop and folders launch quota of 5 per rolling 10 seconds.

### dd.launchers.forget() [#ddlaunchersforget]

```ts
dd.launchers.forget(id: string): Promise<void>
```

Removes the item from the store of every instance of your widget. A widget
with several instances edits its own membership list instead and never calls
this. The host drops the whole store when the widget is uninstalled. Unknown
id: `NOT_FOUND`.

### dd.launchers.onDrag() [#ddlaunchersondrag]

```ts
dd.launchers.onDrag(cb: (drag: { inside: boolean; count: number; kinds: string[] }) => void): () => void
```

An OS drag entered (`inside: true`) or left your drop rect. `count` is the
number of paths in the drag; `kinds` are extension hints only (`app` for
`.exe`, `shortcut` for `.lnk` and `.url`, `item` otherwise), since the host
never touches the filesystem during a hover. Paint your drop ring from this:
the native `dragover` never reaches the page.

### dd.launchers.onDrop() [#ddlaunchersondrop]

```ts
dd.launchers.onDrop(cb: (drop: { items: LauncherItem[] }) => void): () => void
```

The drop landed on you. The items are already in the store; an item that was
there already is announced again so this instance can adopt it. Add the ids
to your membership, re-render, and fetch icons via `icons`. Drops are refused
outright in edit mode and while widgets are peeked away.

### dd.launchers.onChange() [#ddlaunchersonchange]

```ts
dd.launchers.onChange(cb: () => void): () => void
```

The store changed: a drop on any instance of your widget, a `forget`, or an
uninstall. Call `list` again.


# Time, dates, theme, and cursor (/docs/reference/api/environment)



## dd.time [#ddtime]

Needs the `time` permission.

### dd.time.now() [#ddtimenow]

```ts
dd.time.now(): Promise<{ epochMs: number }>
```

### dd.time.onTick() [#ddtimeontick]

```ts
dd.time.onTick(cb: (tick: { epochMs: number }) => void): () => void
```

A second-aligned wall-clock tick. Prefer it over your own `setInterval` for
clock-like widgets: it stays aligned to the real second boundary.

## dd.dates [#dddates]

Always available, no permission needed: calendar arithmetic in local time,
with [date-fns](https://date-fns.org) underneath. The namespace is curated
rather than a re-export, because SDK surface is a permanent contract and the
whole library would cost every widget 73 KB. Display formatting is left out
on purpose: `Intl.DateTimeFormat` and `Intl.NumberFormat` with
`style: "unit"` already localize.

Everything reads and writes local wall-clock fields. Never
`new Date("2027-03-14")`: the date-only string form parses as UTC and lands a
day early west of Greenwich. An Invalid Date passed in comes back out as NaN
or an Invalid Date, the way JS does.

```js
const { startOfDay, breakdown, daysBetween, fromDayKey } = dd.dates;
const target = fromDayKey(settings.date);            // null while unset
if (target) {
  const now = new Date(tick.epochMs);
  const sleeps = daysBetween(now, target);            // 3, not "2 days 23 hours"
  const parts = breakdown(startOfDay(now), target);   // { years: 0, months: 1, days: 14, ... }
}
```

### dd.dates.startOfDay() [#dddatesstartofday]

```ts
dd.dates.startOfDay(d: Date): Date
```

Local midnight of `d`'s day.

### dd.dates.startOfMonth() [#dddatesstartofmonth]

```ts
dd.dates.startOfMonth(d: Date): Date
```

Local midnight of the first day of `d`'s month.

### dd.dates.startOfWeek() [#dddatesstartofweek]

```ts
dd.dates.startOfWeek(d: Date, weekStart?: Weekday): Date

type Weekday = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday"
```

Local midnight of the first day of `d`'s week. `weekStart` is the setting
string as stored, so `startOfWeek(d, settings.weekStart)` reads directly; a
missing or off-list value means Monday.

### dd.dates.addDays() [#dddatesadddays]

```ts
dd.dates.addDays(d: Date, n: number): Date
```

`d` moved `n` whole days at the same wall-clock time, so a daylight-saving
day is one step.

### dd.dates.addMonths() [#dddatesaddmonths]

```ts
dd.dates.addMonths(d: Date, n: number): Date
```

`d` moved `n` whole months, with the day clamped to the month it lands in:
31 January plus one is 28 or 29 February. Time of day is kept.

### dd.dates.daysBetween() [#dddatesdaysbetween]

```ts
dd.dates.daysBetween(from: Date, to: Date): number
```

Whole calendar days from `from` to `to`, midnight to midnight, so a 23-hour
daylight-saving day still counts one. Negative when `to` is the earlier
date. Use it for "3 sleeps away" and for day keys that must turn over at
midnight, not at a 24-hour mark.

### dd.dates.breakdown() [#dddatesbreakdown]

```ts
dd.dates.breakdown(
  from: Date,
  to: Date,
  opts?: { largestUnit?: "years" | "days" },
): { years: number; months: number; days: number; hours: number; minutes: number; seconds: number }
```

The span as calendar parts, every field present (zeros included). It is
calendar arithmetic, not division: 31 January to 28 February is one month,
31 January to 1 March in a leap year is one month and one day, and 12:00 to
12:00 across a spring-forward is one day and zero hours. Sub-second
remainders truncate. When `to` is before `from`, every non-zero part is
negative, so order the pair yourself when you want a size and a direction.

`largestUnit: "days"` folds years and months into the day count, which is
the shape a live "days hours minutes seconds" readout wants.

### dd.dates.dayKey() [#dddatesdaykey]

```ts
dd.dates.dayKey(d: Date): string
```

Local `"YYYY-MM-DD"` for `d`. Fixed width, so two keys compare as dates with
`<` and `>`, and a key never passes through UTC. The right shape for storage
keys and for a `date` setting's value.

### dd.dates.fromDayKey() [#dddatesfromdaykey]

```ts
dd.dates.fromDayKey(key: string): Date | null
```

Local midnight of a `dayKey`, or `null` when the key is malformed or names a
day that does not exist (`"2027-02-31"` is refused rather than read as
3 March). A `date` setting's value goes through this; guard the `null`.

## dd.theme [#ddtheme]

Always available, no permission needed. Themes are applied by the host; the
SDK re-applies the token map to `:root` automatically, so plain CSS using
`var(--dd-*)` restyles itself. These methods are for JS that resolves colors
manually (canvas, inline styles).

### dd.theme.tokens() [#ddthemetokens]

```ts
dd.theme.tokens(): Promise<Record<string, string>>
```

The current `--dd-*` token map for this widget instance, as the host knows
it: the theme plus any per-widget style the user set on this instance. Note:
manifest
[font pins](/docs/reference/manifest#fonts) are injected as CSS on top of
this map, so `tokens()` does not see them; `dd.ui.token()` reads computed
style and does. Resolve font faces through `dd.ui.token()`.

### dd.theme.onChange() [#ddthemeonchange]

```ts
dd.theme.onChange(cb: (tokens: Record<string, string>) => void): () => void
```

Fires live on every theme change (theme picker, wallpaper auto-theme, user
overrides) and on every per-widget style edit for this instance, previews
included. If you resolve token values in JS, do not cache them across this
event: re-read and redraw.

Since app 0.4.0 theme colors fade on screen, about 0.65 s at normal Animation
speed. `cb` runs once the fade has settled, so computed styles already hold
the new values when it fires. Without a fade (Animation speed Off, reduced
motion, low-spec mode, or changes less than 300 ms apart, as in every live
preview) it runs at once. Changes that arrive during one fade may coalesce
into a single call.

### dd.theme.accent() [#ddthemeaccent]

```ts
dd.theme.accent(settings: Record<string, unknown>): string
```

Resolves the `accentColor`
[settings preset](/docs/reference/manifest#setting-presets) pair: the theme
accent while `useAccent` is on (the default), the user's custom `color`
otherwise. It reads the current token value, so re-call it inside
`dd.theme.onChange` (or use `bindAccent`) to stay theme-reactive.

### dd.theme.bindAccent() [#ddthemebindaccent]

```ts
dd.theme.bindAccent(cb: (color: string) => void): Promise<() => void>
```

`settings.bind` and `theme.onChange` in one: `cb` fires with the resolved
accent immediately and again on every settings or theme change. The returned
promise resolves (with the combined unsubscribe) after that first call.

## dd.cursor [#ddcursor]

Always available, no permission needed, and it works for non-interactive
widgets too: the window stays click-through, you get the signal without
capturing input.

### dd.cursor.onHover() [#ddcursoronhover]

```ts
dd.cursor.onHover(cb: (state: { inside: boolean }) => void): () => void
```

Fires when the real cursor enters or leaves this widget's footprint. While a
popout is open, the footprint is the bounding box of widget plus pane
(crossing the gap between them never flickers a leave), and both documents
receive the event.

### dd.cursor.onMove() [#ddcursoronmove]

```ts
dd.cursor.onMove(cb: (pos: { x: number; y: number }) => void): () => void
```

Cursor position at roughly 30 Hz while the cursor is inside this widget's (or
its pane's) rect, as fractions (0..1) of the receiving document's viewport.
For element-level hover effects without input capture. For the common case,
skip the subscription entirely: mark elements `data-dd-hover` and the SDK
toggles a `dd-hover` class on them for you.


# Media and audio (/docs/reference/api/media-audio)



## dd.media [#ddmedia]

Reads need the `media` permission; the transport verbs additionally need
`mediaControls`.

### dd.media.status() [#ddmediastatus]

```ts
dd.media.status(): Promise<NowPlaying | null>
```

The latest now-playing snapshot, a cheap cache read (`null` until the
watcher's first pass). Seed your UI with this, then follow the stream.

### dd.media.onNowPlaying() [#ddmediaonnowplaying]

```ts
dd.media.onNowPlaying(cb: (np: NowPlaying) => void): () => void

interface NowPlaying {
  /** False = no media session anywhere (fields null, can* false). */
  hasSession: boolean;
  status: "playing" | "paused" | "stopped";
  title: string | null;
  artist: string | null;
  album: string | null;
  /** Source app id (e.g. "Spotify.exe"): for feature detection, not UI. */
  sourceApp: string | null;
  /** Album art as an image data URI, or null. */
  art: string | null;
  /** Null when the player reports no usable duration (live streams). */
  position: {
    /** Seconds from track start as of asOfEpochMs. */
    seconds: number;
    durationSeconds: number;
    /** The player's own timeline stamp (epoch ms). Null = don't interpolate. */
    asOfEpochMs: number | null;
  } | null;
  canPlayPause: boolean;
  canSkipNext: boolean;
  canSkipPrevious: boolean;
  canSeek: boolean;
}
```

Two things to know:

* **Selection policy.** The host picks the actually playing session across
  all Windows media sessions, not Windows' own "current session" (which
  favors whoever last touched the media keys, like a paused YouTube tab). It
  falls back to the most recent paused session so a play button works from
  the desktop. `hasSession: false` means no session anywhere.
* **Change-driven, not fixed-rate.** Events arrive only when the reported
  state changes. Seed with `status()`, and while `status === "playing"`
  interpolate the displayed position locally as
  `seconds + (Date.now() - asOfEpochMs) / 1000` (`asOfEpochMs` null means
  don't interpolate).

`title`, `artist`, `album`, `art` and `position` are independently nullable;
render what you have.

### Transport controls [#transport-controls]

```ts
dd.media.playPause(): Promise<void>   // 20 calls / 10 s
dd.media.next(): Promise<void>        // 30 calls / 10 s
dd.media.previous(): Promise<void>    // 30 calls / 10 s
dd.media.seek(seconds: number): Promise<void>  // 60 calls / 10 s
```

All act on the selected session. A player rejecting an action (or the
session vanishing mid-call) is a silent no-op; the next snapshot corrects
your UI. Nothing to control rejects `NOT_FOUND`. `seek` takes seconds from
track start, clamped to the live track end host-side; check `canSeek` first,
and send on slider release rather than every drag tick.

## dd.audio [#ddaudio]

Needs the `audio` permission. Event-only: there are no audio methods.

### dd.audio.onSpectrum() [#ddaudioonspectrum]

```ts
dd.audio.onSpectrum(cb: (frame: SpectrumFrame) => void): () => void

interface SpectrumFrame {
  bins: number[];
}
```

500 log-spaced loudness bands (roughly 35 Hz to 16 kHz), each 0..1, derived
host-side from system audio output. Never raw samples: loudness bands only,
and speech is not reconstructible from them.

Delivery contract:

* Frames arrive at roughly 30 Hz while media plays (20 Hz when the host runs
  in low performance mode).
* A single all-zero frame marks each transition to silence (pause, track gap,
  capture stopping), then the stream goes quiet. Let your bars decay to zero
  on it and stop drawing.
* Capture is demand-driven: it runs only while some enabled, visible widget
  with the `audio` grant exists (the active taskbar counts as one, since app
  0.3.0) and the host's media watcher reports a playing session.
  Subscribing is what turns it on. Sources without a media session
  (audiobook players, most games) do not animate it.
* Since app 0.2.4 a widget may opt out of the media gate: declare a boolean
  setting with the key `anyAudio` (default false). The host reads it by
  name; while any visible audio widget has it on, capture runs for all
  system sound instead of only media sessions. The checkbox applies on
  Save, and real silence still produces the zero frame.

Performance contract: do not draw in the callback. Copy `bins` into a reused
buffer and render in your own `requestAnimationFrame` loop, interpolating
between frames (fast attack, slow decay), and stop the loop once the bars
settle at zero.

```js
const levels = new Float32Array(500);
dd.audio.onSpectrum((frame) => {
  latest = frame.bins;      // stash
  ensureRafLoop();          // draw elsewhere, at your own pace
});
```


# Notifications and dialogs (/docs/reference/api/notifications-dialogs)



## dd.notify [#ddnotify]

Needs the `notifications` permission.

### dd.notify.sync() [#ddnotifysync]

```ts
dd.notify.sync(entries: {
  id: string;      // 1..64 chars, unique within the list
  at: number;      // epoch ms, between now and 366 days ahead
  title: string;   // non-empty, up to 80 chars
  body?: string;   // up to 200 chars
}[]): Promise<{ scheduled: number }>
```

Replaces this instance's entire schedule of future notifications. Cancelling
one means omitting it; an empty array clears them all. There is no per-entry
add or remove.

The host keeps the schedule and raises each toast itself, which is the whole
point: a hidden or disabled widget stops running, and the app may restart,
but the reminder still arrives. Re-send the list whenever it changes, and
once on boot.

Behavior and limits:

* Caps are rejected, never silently clamped: 64 entries, `at` within a year,
  title up to 80 characters, body up to 200. Control characters are stripped
  host-side.
* An entry more than 15 minutes past its time is dropped unfired rather than
  stacking, so a machine that slept for a week does not wake to a week of
  toasts.
* The toast is a host-drawn window in the corner of the primary work area.
  The widget supplies text, never markup, and does not control placement,
  appearance, or timing beyond `at`. At most 3 toasts are on screen at once.
* Removing or disabling the instance drops its schedule. Hiding deliberately
  does not: surviving an unmount is the point.

## dd.dialogs [#dddialogs]

Host-drawn picker windows. Both pickers share the same consent gates and
lifecycle:

* Requires the real cursor inside the widget (call from a click handler;
  for a bar, the bar window or its pane, app 0.3.7 or newer), and never
  opens in edit mode.
* The picker is a normal OS window (taskbar, Alt-Tab, minimizable) that the
  host renders and owns; the widget never draws into it, and only ever
  receives ids back.
* No client timeout: the promise resolves whenever the user answers. The
  host force-settles it after 1 hour unanswered, and force-cancels on edit
  mode, widget reload, hide, disable, remove, or a crash.
* One picker per kind and instance. Calling again while your picker is open
  surfaces the existing window, and the new call resolves `cancelled`
  immediately.
* `checked` pre-checks ids (up to 1024, each up to 64 chars): pass your
  current selection.
* The result is a replacement set: `cancelled: true` means change nothing;
  otherwise `ids` is the user's full new checked set, and unchecking is
  meaningful. Checked ids that no longer exist are carried through
  untouched.
* 5 opens per rolling 10 seconds, shared between both pickers.

### dd.dialogs.pickDesktopItems() [#dddialogspickdesktopitems]

```ts
dd.dialogs.pickDesktopItems(opts?: { checked?: string[] }): Promise<{
  cancelled: boolean;
  ids: string[];
}>
```

Needs the `desktop` permission. Picks from the items on the user's desktop
(the same set `dd.desktop.list` returns).

### dd.dialogs.pickFolderItems() [#dddialogspickfolderitems]

```ts
dd.dialogs.pickFolderItems(settingKey: string, opts?: { checked?: string[] }): Promise<{
  cancelled: boolean;
  ids: string[];
}>
```

Needs the `folders` permission. The same picker aimed at the folder the
named `filePath` setting resolves to. A blank setting value falls through to
the desktop picker behind its own `desktop` grant, so a drawer widget keeps
one code path.


# Settings, storage, and files (/docs/reference/api/settings-storage)



## dd.settings [#ddsettings]

Reading settings needs no permission. Fields are declared in the manifest
(see [the manifest reference](/docs/reference/manifest#settings)); values are
edited by the user in the shell's settings dialog, never written by the
widget. A widget may however ask the host to open that dialog from a click,
with [`dd.settings.open()`](#ddsettingsopen).

### dd.settings.get() [#ddsettingsget]

```ts
dd.settings.get(): Promise<Record<string, unknown>>
```

The effective settings: manifest defaults merged with the user's stored
values. Always returns the persisted state, even while the settings dialog
previews something else.

### dd.settings.onChange() [#ddsettingsonchange]

```ts
dd.settings.onChange(cb: (settings: Record<string, unknown>) => void): () => void
```

Fires on any settings edit. While the user tries values in the settings
dialog it also fires with live-previewed (unsaved) values, and fires once
more with the stored values if they dismiss. Treat every event the same and
just re-apply; do not try to distinguish preview from commit.

### dd.settings.bind() [#ddsettingsbind]

```ts
dd.settings.bind(cb: (settings: Record<string, unknown>) => void): Promise<() => void>
```

Load-and-listen in one call: `cb` fires once with the current settings and
again on every change. The returned promise resolves (with the unsubscribe
function) after that first call, so `await dd.settings.bind(cb)` when the
code below needs settings populated.

### dd.settings.open() [#ddsettingsopen]

```ts
dd.settings.open(opts?: { highlight?: string }): Promise<null>
```

Opens this widget's own settings dialog, the same window the edit-mode gear
opens. `highlight` names one of your manifest setting keys; the dialog
scrolls to that field and flashes it. Useful from an empty state: a "set me
up" button that lands the user on the right field.

The gates, shared with the other host-window methods:

* Requires the real cursor inside the widget (call it from a click handler;
  for a bar, the bar window or its pane, app 0.3.7 or newer), and never
  opens in edit mode. On a bar the call opens the Taskbar settings window
  instead. A call outside those conditions rejects
  `PERMISSION_DENIED`; there is no way to open the dialog without a user
  interaction.
* `highlight` must name one of your setting keys (expanded preset fields
  included), or the call rejects `INVALID_PARAMS`. A field that is currently
  hidden (for example `panelImage` while `panel` is not `image`) still opens
  the dialog, just without the scroll-and-flash.
* One window per instance: a repeat call surfaces the already-open window
  and replays the flash.
* The promise resolves once the window is up. It never waits for Save; watch
  `dd.settings.onChange` for the values as usual.
* Rate limited to 5 opens per rolling 10 seconds, shared with `dd.dialogs`.
* App 0.2.6 or newer; from a bar or its pane, 0.3.7 or newer.

This does not change what settings are: the dialog stays the only writer,
and there is still no `dd.settings.set`.

## dd.storage [#ddstorage]

Needs the `storage` permission. A small per-instance key-value store,
persisted by the host; two instances of the same widget do not share it.
A [taskbar](/docs/guides/taskbars#storage-without-an-instance) has no
instance, so it gets one store per bar widget instead (app 0.4.1 or newer).

```ts
dd.storage.get(key: string): Promise<unknown>          // stored value or null
dd.storage.set(key: string, value: unknown): Promise<void>
dd.storage.delete(key: string): Promise<void>
```

Limits: a value is capped at 64 KB serialized, the whole store at 512 KB per
instance. Exceeding either rejects `PAYLOAD_TOO_LARGE`. This is state
storage, not a database; keep large data out of it.

## dd.files [#ddfiles]

Needs the `files` permission.

### dd.files.read() [#ddfilesread]

```ts
dd.files.read(settingKey: string): Promise<{
  text: string;
  sizeBytes: number;
  modifiedEpochMs?: number;
}>
```

Reads the file the user configured in the named `filePath` setting of this
widget. Widgets never handle raw paths: you name your own setting key, the
host resolves the path the user granted there. Manifest defaults for
`filePath` fields are deliberately ignored, so there is no way to ship a
widget pre-pointed at someone's file. Files are capped at 1 MB.

## dd.image [#ddimage]

Needs NO permission: it follows the same consent model as `dd.files.read`
(only a file the user actively picked in one of this widget's `filePath`
settings can ever be read), and returns bounded, content-sniffed image data
rather than file contents. App 0.2.4+.

### dd.image.load() [#ddimageload]

```ts
dd.image.load(settingKey: string): Promise<{
  dataUrl: string;
  mime: string;
  sizeBytes: number;
  modifiedEpochMs?: number;
}>
```

The image the user picked in the named `filePath` setting, as a base64
`data:` URL ready for `<img src>` or CSS `url()` (the widget CSP allows
`img-src data:`). Accepts png, jpeg, webp and gif, sniffed by magic bytes,
never by extension; capped at 8 MB; rate limit 20 per 10 s. This is what
backs the `panelStyle` preset's Background image field and the Sticker
widget.

## How secrets work [#how-secrets-work]

A `secretRef` setting names a secret stored by the host; the user picks which
one in the settings dialog, and the secret's value never reaches widget code.
The only place a widget can use one is `dd.http.fetch`: pass
`{ "$secret": "settingKey" }` as a header or query value and the host injects
the real value after validating the request. See
[dd.http.fetch](/docs/reference/api/data-services#ddhttpfetch) for the exact
rules. There is no API that returns a secret's value.


# System and power (/docs/reference/api/system)



## dd.system [#ddsystem]

Reads need the `system` permission; the write surface (`setVolume`,
`setMuted`, the microphone verbs, `setRadio`, the audio device and app
volume verbs, the Bluetooth verbs, the brightness verbs, the Wi-Fi verbs,
`openSettings`) additionally needs `systemControls`.

### dd.system.status() [#ddsystemstatus]

```ts
dd.system.status(): Promise<SystemVitals | null>
```

The latest vitals snapshot, a cheap cache read (`null` until the sampler's
first pass). Seed your UI with this, then follow `onVitals`.

### dd.system.onVitals() [#ddsystemonvitals]

```ts
dd.system.onVitals(cb: (vitals: SystemVitals) => void): () => void

interface SystemVitals {
  /** CPU busy fraction 0..1 (null on the very first sample). */
  cpu: number | null;
  ram: { usedMb: number; totalMb: number; percent: number } | null;
  /** GPU busy fraction 0..1: the busiest engine (3D, copy, video, compute)
   *  of ONE adapter, the figure Task Manager shows. The host measures the
   *  hardware adapter with the most dedicated video memory unless the user
   *  picked another in Settings > General. App 0.3.8+; earlier apps summed
   *  the 3D engines of every adapter. */
  gpu: number | null;
  /** Disk busy fraction 0..1 (physical disks total). */
  disk: number | null;
  /** Fixed drives (letters), resampled about every 30s inside the 1Hz
   *  stream. `percent` is used/total 0..1; `label` is the volume name or
   *  null; null when enumeration fails, empty when no fixed drives exist. */
  drives:
    | { mount: string; label: string | null; usedMb: number; totalMb: number; percent: number }[]
    | null;
  net: { rxBps: number; txBps: number } | null;
  battery: { percent: number | null; charging: boolean } | null;
  wifi: { ssid: string; signal: number } | null;
  volume: { level: number; muted: boolean } | null;
  /** The default microphone; null with no microphone. App 0.4.2+. */
  mic: { level: number; muted: boolean } | null;
  /** Radio state as Windows Quick Settings shows it; null while Windows
   *  cannot list the radios. App 0.4.2+. */
  radios: { wifi: RadioState; bluetooth: RadioState; cellular: RadioState } | null;
}

/** "disabled": off by hardware switch or airplane mode. null: no such radio. */
type RadioState = "on" | "off" | "disabled" | null;
```

A 1 Hz stream. Every field fails independently: a machine with no GPU
counters, no Wi-Fi adapter, no battery, or no audio device reports `null` for
that field while the rest keep flowing. Render nulls as absent, not as zero.
`cpu`, `gpu`, `disk`, `volume.level`, `mic.level` and `ram.percent` are 0..1 fractions;
`wifi.signal` is 0..100. `drives` lists fixed volumes only and refreshes about
every 30 seconds inside the stream (app 0.2.5+); sizes are MiB, like `ram`.
`net` sums the up hardware interfaces only, so a VPN tunnel or a virtual
switch port never counts the same bytes again; a machine with no hardware
interface up (a VM guest) sums every up interface, and the first tick after
that rule flips is `null` (app 0.3.7 or newer; earlier apps summed every up
interface).
`gpu` is the busiest engine of one graphics adapter, the figure Task Manager
shows for that card: the hardware adapter with the most dedicated video
memory, or the one the user picked in Settings > General (app 0.3.8 or newer;
earlier apps summed the 3D engines of every adapter, integrated graphics
included).

### dd.system.setVolume() [#ddsystemsetvolume]

```ts
dd.system.setVolume(level: number): Promise<void>  // 0..1
```

Master volume. 120 calls per rolling 10 seconds; throttle slider drags
client-side to roughly one call per 100 ms.

### dd.system.setMuted() [#ddsystemsetmuted]

```ts
dd.system.setMuted(muted: boolean): Promise<void>
```

20 calls per rolling 10 seconds.

### dd.system.setMicVolume() [#ddsystemsetmicvolume]

```ts
dd.system.setMicVolume(level: number): Promise<void>  // 0..1
```

The default microphone's level, the one the `mic` vitals field reads. 120
calls per rolling 10 seconds; throttle slider drags like `setVolume`. A
machine with no microphone rejects with `INTERNAL`. App 0.4.2 or newer.

### dd.system.setMicMuted() [#ddsystemsetmicmuted]

```ts
dd.system.setMicMuted(muted: boolean): Promise<void>
```

Mutes or unmutes the default microphone. 20 calls per rolling 10 seconds.
App 0.4.2 or newer.

### dd.system.setRadio() [#ddsystemsetradio]

```ts
dd.system.setRadio(
  kind: "wifi" | "bluetooth" | "cellular",
  on: boolean,
): Promise<RadioSetResult>

interface RadioSetResult {
  access: "allowed" | "deniedByUser" | "deniedBySystem" | "unspecified";
  state: RadioState;
}
```

Turns every radio of a kind on or off, like the tiles in Windows Quick
Settings. Call it from a click: the host refuses it in edit mode and unless
the cursor is inside your widget, its pane, or your bar, because Wi-Fi off
cuts the network.

Windows has its own privacy switch, "Let apps control device radios". When
it is off, the call resolves with `access: "deniedByUser"` instead of
rejecting; show that and offer `openSettings("privacy-radios")`. `state` is
the radio's state right after the switch, and the `radios` vitals field
confirms it within a second. A machine with no radio of that kind rejects
with `NOT_FOUND`. 5 calls per rolling 10 seconds. App 0.4.2 or newer.

### dd.system.audioDevices() [#ddsystemaudiodevices]

```ts
dd.system.audioDevices(): Promise<AudioDevices>

interface AudioDevices {
  outputs: AudioDevice[];
  inputs: AudioDevice[];
}

interface AudioDevice {
  /** Opaque token for setAudioDevice, valid for this app run only. */
  id: string;
  /** The name Windows shows, e.g. "Speakers (Realtek Audio)". */
  name: string;
  /** The Windows default for its direction. */
  isDefault: boolean;
}
```

The active audio outputs and inputs, each list with the default first, then
by name. The hardware id never reaches your widget: `id` is a token the host
issues per app run, so list again after a restart. 20 calls per rolling 10
seconds. App 0.4.2 or newer.

### dd.system.setAudioDevice() [#ddsystemsetaudiodevice]

```ts
dd.system.setAudioDevice(id: string): Promise<AudioDevices>
```

Makes a listed device the Windows default for its direction, in every role
(console, multimedia, communications), as the Windows sound menu does. It
resolves with the fresh list as Windows now reports it: render that, not a
guess. A stale id rejects `NOT_FOUND` (list again). The volume calls and
the audio spectrum follow the new default by themselves. 5 calls per
rolling 10 seconds. App 0.4.2 or newer.

### dd.system.onAudioDevicesChanged() [#ddsystemonaudiodeviceschanged]

```ts
dd.system.onAudioDevicesChanged(cb: () => void): () => void
```

A data-free ping when a device comes, goes, is enabled or disabled, or
becomes the default (your own `setAudioDevice` included), at most once a
second: call `audioDevices()` again. Needs `system` and `systemControls`.
App 0.4.2 or newer. For a ready-made list, use
[`<dd-audio-devices>`](/docs/reference/components#dd-audio-devices).

### dd.system.audioApps() [#ddsystemaudioapps]

```ts
dd.system.audioApps(): Promise<{ apps: AudioApp[] }>

interface AudioApp {
  /** Opaque token, valid for this app run. */
  id: string;
  /** "Spotify"; "System sounds" for the system session. */
  name: string;
  /** The app's icon as a data: URI, or null. */
  icon: string | null;
  /** The app's own level 0..1, a fraction of the master volume. */
  level: number;
  muted: boolean;
  /** Playing sound right now. */
  active: boolean;
  /** The Windows system sounds session. */
  system: boolean;
}
```

The apps with sound on the default output device, as the Windows volume
mixer shows them: one row per app, playing apps first, then by name, and the
system sounds row last. Running app names and icons say more than the vitals
do, so this read needs `systemControls` too. There is no change event: poll
while your mixer is visible, or use
[`<dd-audio-mixer>`](/docs/reference/components#dd-audio-mixer), which does.
20 calls per rolling 10 seconds. App 0.4.2 or newer.

### dd.system.setAppVolume() [#ddsystemsetappvolume]

```ts
dd.system.setAppVolume(id: string, level: number): Promise<void>  // 0..1
```

Sets one app's level, every stream it has. 120 calls per rolling 10
seconds; throttle slider drags like `setVolume`. A stale id rejects with
`NOT_FOUND` (list again). App 0.4.2 or newer.

### dd.system.setAppMuted() [#ddsystemsetappmuted]

```ts
dd.system.setAppMuted(id: string, muted: boolean): Promise<void>
```

Mutes or unmutes one app. 20 calls per rolling 10 seconds. App 0.4.2 or
newer.

### dd.system.bluetoothDevices() [#ddsystembluetoothdevices]

```ts
dd.system.bluetoothDevices(): Promise<{ devices: BluetoothDevice[] }>

interface BluetoothDevice {
  /** Opaque token, valid for this app run. */
  id: string;
  /** The name Windows shows. */
  name: string;
  connected: boolean;
  /** "audio": headsets and speakers; the rest from the device class. */
  kind: "audio" | "input" | "phone" | "computer" | "other";
  /** bluetoothConnect / bluetoothDisconnect work on this device. */
  canConnect: boolean;
}
```

The paired Bluetooth devices, classic and Low Energy (a dual-mode device is
one row), connected first. It reads what Windows already knows: no scan, and
no pairing. Send users to `openSettings("bluetooth")` to add a device. The
list stays readable while the radio is off. Paired device names say more
than the vitals do, so this read needs `systemControls` too. There is no
change event: poll while your list is visible, or use
[`<dd-bluetooth-devices>`](/docs/reference/components#dd-bluetooth-devices),
which does. 20 calls per rolling 10 seconds. App 0.4.2 or newer.

### dd.system.bluetoothConnect() and bluetoothDisconnect() [#ddsystembluetoothconnect-and-bluetoothdisconnect]

```ts
dd.system.bluetoothConnect(id: string): Promise<void>
dd.system.bluetoothDisconnect(id: string): Promise<void>
```

Connect or disconnect a paired audio device (`canConnect: true`), like the
Connect button in Windows Quick Settings; disconnecting does not unpair.
Call them from a click: the host refuses them in edit mode and unless the
cursor is inside your widget, its pane, or your bar. They resolve once
Windows has the request, and the link follows within a few seconds, so list
again. A device with no audio connection rejects with `INVALID_PARAMS`, a
stale id with `NOT_FOUND`. 5 calls per rolling 10 seconds each. App 0.4.2 or
newer.

### dd.system.brightness() [#ddsystembrightness]

```ts
dd.system.brightness(): Promise<{ displays: DisplayBrightness[] }>

interface DisplayBrightness {
  /** Opaque token, valid for this app run. */
  id: string;
  /** "LG ULTRAGEAR"; "Built-in display" for a nameless laptop panel. */
  name: string;
  /** 0..1 */
  level: number;
  primary: boolean;
  /** The display showing your widget, bar or pane. */
  here: boolean;
}
```

The displays whose brightness can be set: a laptop's built-in panel, and
external monitors that answer DDC/CI over the cable. A monitor without
DDC/CI, or with it turned off in its own menu, is left out, so a desktop
often gets an empty list; render nothing then. There is no change event:
poll while visible, or use
[`<dd-brightness>`](/docs/reference/components#dd-brightness), which does.
Monitor reads are cached for 15 seconds, so polling faster shows nothing
new. 20 calls per rolling 10 seconds. App 0.4.2 or newer.

### dd.system.setBrightness() [#ddsystemsetbrightness]

```ts
dd.system.setBrightness(id: string, level: number): Promise<void>  // 0..1
```

Sets one display's brightness. The host serves every display from one
queue: sets for the same display collapse to the newest, and writes to an
external monitor stay 200 ms apart, because many monitors store each write.
Throttle slider drags to roughly one call per 150 ms. A stale id rejects
with `NOT_FOUND` (list again). 120 calls per rolling 10 seconds. App 0.4.2
or newer.

### dd.system.wifiNetworks() [#ddsystemwifinetworks]

```ts
dd.system.wifiNetworks(): Promise<{ networks: WifiNetwork[] }>

interface WifiNetwork {
  ssid: string;
  /** Signal quality 0..100. */
  signal: number;
  secured: boolean;
  connected: boolean;
  /** Windows has a saved profile: wifiConnect only works for these. */
  known: boolean;
}
```

Available networks, deduped by SSID (strongest kept), connected first. The
host triggers a background scan at most every 30 seconds. 6 calls per rolling
10 seconds.

### dd.system.wifiConnect() [#ddsystemwificonnect]

```ts
dd.system.wifiConnect(ssid: string): Promise<void>
```

Connects only to networks with a saved Windows profile (`known: true`);
passwords never cross the bridge. No profile rejects `NOT_FOUND`; send users
to `openSettings("wifi")` for new networks. 3 calls per rolling 10 seconds.

### dd.system.wifiDisconnect() [#ddsystemwifidisconnect]

```ts
dd.system.wifiDisconnect(): Promise<void>
```

3 calls per rolling 10 seconds.

### dd.system.openSettings() [#ddsystemopensettings]

```ts
dd.system.openSettings(panel: SettingsPanel): Promise<void>

type SettingsPanel =
  | "sound"
  | "power"
  | "wifi"
  | "battery"
  | "bluetooth"      // app 0.4.2+
  | "display"        // app 0.4.2+
  | "apps-volume"    // app 0.4.2+: the per-app volume mixer page
  | "privacy-radios" // app 0.4.2+: where Windows lets apps control the radios
```

Opens a Windows settings page. `panel` is a whitelist key mapped host-side;
widgets can never launch arbitrary settings targets. An app older than 0.4.2
rejects the newer keys with `INVALID_PARAMS`. 3 calls per rolling 10
seconds.

## dd.power [#ddpower]

Needs the `power` permission, which is deliberately separate from
`systemControls`: granting a volume slider is not granting the power to turn
the machine off.

### dd.power.capabilities() [#ddpowercapabilities]

```ts
dd.power.capabilities(): Promise<{ sleep: boolean; hibernate: boolean }>
```

What this machine can actually do, read from its firmware. Check it before
drawing a Sleep button: many desktops have no sleep state at all, and a
button that fails every press reads as a broken widget. An unreadable answer
reports `true` for both, so a machine that would have worked never loses its
button. No gesture gate (a widget needs this before any interaction). 30
calls per rolling 10 seconds.

### dd.power.run() [#ddpowerrun]

```ts
dd.power.run(action: "shutdown" | "restart" | "restartAdvanced" | "sleep" | "lock" | "signOut"): Promise<{ ran: boolean }>
```

Ends or suspends the Windows session. Only these six actions exist; nothing
else (no flags, no timeouts, no targets) reaches the OS. Refused unless the
real cursor is inside the widget, and refused in edit mode, so a background
timer can never trigger it. From a bar, the test is the bar window or its
pane (app 0.3.7 or newer; older apps refuse bars outright).

`restartAdvanced` restarts into the advanced startup menu (recovery,
firmware settings, safe mode), what `shutdown /r /o` does. App 0.3.7 or
newer; an older app rejects it `INVALID_PARAMS` with "unknown power action",
so catch that to feature-detect.

`shutdown`, `restart`, `restartAdvanced` and `signOut` first raise a host-drawn confirmation the
widget cannot draw, style, or auto-answer; Cancel holds the initial focus,
and Escape, closing the window, or a timeout all answer no. `ran: false`
means the user declined, which is the moment to re-enable your button:

```js
const { ran } = await dd.power.run("shutdown");
if (!ran) button.disabled = false;
```

`sleep` and `lock` skip the confirmation, being instantly reversible. Nothing
here forces applications to close: an app with unsaved work still gets
Windows' own "this app is preventing shutdown" screen. On actions that
proceed, the session usually ends before the promise settles. 6 calls per
rolling 10 seconds.


# UI toolkit (/docs/reference/api/ui)



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

```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() [#dduicreate]

Builds a DOM element.

```ts
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.

| Prop                                                                         | Behavior                                                                       |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `class`                                                                      | sets `className` (write `class`, not `className`)                              |
| `style`                                                                      | object: merged into `el.style` (camelCase keys); string: the `style` attribute |
| `on*` (`onClick`, `onPointerDown`, ...)                                      | `addEventListener` with the lowercased remainder; never reactive               |
| `ref: (el) => void`                                                          | called after props and children are applied, for elements you keep a handle to |
| key contains `-` (`data-*`, `aria-*`, `stroke-width`), or the element is SVG | attribute: `true` becomes `""`, `false` and nullish remove it                  |
| anything else                                                                | element 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 [#signals]

```ts
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() [#dduibindparts]

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.

```ts
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.

```js
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.autofit() [#dduiautofit]

```ts
autofit(root: HTMLElement, opts?: { content?: HTMLElement; prop?: string }):
  { refit(): void; stop(): void }
```

Measure-driven autoscale for a `.dd-autoscale` readout, for a panel whose
type IS the layout. The skeleton's `cqmin` default ties the font to the
container's smaller edge whichever axis that is, so trimming slack height
shrinks a width-limited line for nothing. `autofit` instead measures the
content and writes the largest font-size at which it fits the root BOTH
ways: a shorter tile keeps its type until the text would truly overflow.

`root` is the `.dd-autoscale` element, `content` (default: the root's first
element child) is the measured block, and `prop` (default
`--dd-autoscale-size`) is the custom property written, in px, onto the
root's style. Point `prop` at your own variable to chain a multiplier:

```js
const fit = dd.ui.autofit(rootEl, { prop: "--cd-fit" });
// CSS: --dd-autoscale-size: calc(var(--cd-fit, 23cqmin) * var(--cd-scale, 1));
```

The measurement is a hidden clone of `content`, laid out for one frame at
`max-content` width inside the root, so a wrapping row measures unwrapped
and the fit is a single ratio, no iteration. Everything in the skeleton is
em of one font, so the result is exact, and multipliers riding on the
written value cancel out of the ratio: re-measures converge and go quiet.

Both elements are watched with a `ResizeObserver`, and calls coalesce to
one measurement per frame. One case needs a hand: a content change the
boxes cannot betray, such as text swapped for longer text on the same line
count. Call `refit()` there (the Countdown widget does it whenever its
readout text changes). Intended for type-shaped content: the clone is
cheap for a readout, not for a thousand-row list.

## dd.ui.token() [#dduitoken]

```ts
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. While a theme color is
fading (app 0.4.0+), it answers the color the fade is heading to, never an
in-between value. `fallback` is returned when the token is unset. Unlike `dd.theme.tokens()`, this sees
[manifest font pins](/docs/reference/manifest#fonts), so canvas text should
resolve faces through it. Both see the user's per-widget style for this
instance.

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

## dd.ui.pressGuard() [#dduipressguard]

```ts
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:

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

## dd.ui.components [#dduicomponents]

The `dd-*` custom element constructors, for subclassing: `DdElement`,
`DdPanel`, `DdText`, `DdIcon`, `DdButton`, `DdIconButton`, `DdChip`,
`DdMeter`, `DdAppTile`, `DdVolumePill`. See
[the component reference](/docs/reference/components#overriding) for the
override pattern.


# Popouts, peers, and widgets (/docs/reference/api/widgets-together)



## dd.popout [#ddpopout]

Needs a manifest `popout` declaration
(see [the manifest reference](/docs/reference/manifest#popout)): a geometry
capability, not a permission. A popout is a second document from your widget
folder (the manifest's `popout.entry`), rendered by the host in an overlay
next to your widget. Your own document never moves or resizes.

### dd.popout.open() [#ddpopoutopen]

```ts
dd.popout.open(opts: {
  size: { w: number; h: number };          // your own CSS px, 8..4096 per dimension
  anchor?: Element | { x: number; y: number; w: number; h: number };
  prefer?: "down" | "up" | "left" | "right" | "auto";
  data?: unknown;                          // JSON, up to 8 KB
}): Promise<{
  side: "down" | "up" | "left" | "right";  // the applied side
  size: { w: number; h: number };          // the granted pane size, in your px
}>
```

Opens the pane at exactly `size` (capped by the manifest's `popout.max`,
which is in base grid cells), placed next to `anchor`: an element (the SDK
measures it for you) or a pixel rect in your viewport; omitted means your
whole rect. Size and anchor are in your widget's own pixels, the ones you
measure inside your document. Below a Widget size of 100% the host lays
your document out larger than its tile and scales it down, and it converts
a popout request at that same boundary, so the pane paints at your widget's
scale and the granted size comes back in your pixels. The host tries `prefer`, flips to
the opposite side when that fits better, and nudges the pane fully
on-canvas. Size is granted exactly, position is best-effort, so the pane
may end up overlapping your widget (it paints on top).

`data` reaches the pane document as `ctx.popout.data`. Re-calling reshapes:
the pane document remounts with the new data, and its entrance animation
replays.

Gates: the real cursor must be inside the widget (popouts open only as part
of a user interaction), never in edit mode, main document only. Popouts are
transient and never persisted. 20 calls per rolling 10 seconds.

### dd.popout.close() [#ddpopoutclose]

```ts
dd.popout.close(): Promise<void>
```

Idempotent, callable from either document. The host also force-closes the
pane on edit mode entry, widget reload, grid rescale, hide, disable, remove,
and a crash of either document.

### dd.popout.onChange() [#ddpopoutonchange]

```ts
dd.popout.onChange(cb: (state: { active: boolean; side?: string | null }) => void): () => void
```

The popout lifecycle, including host force-closes. `active: false` means the
pane is gone: un-highlight your trigger. Both documents receive it.

### dd.popout.send() and onMessage() [#ddpopoutsend-and-onmessage]

```ts
dd.popout.send(msg: unknown): void
dd.popout.onMessage(cb: (msg: unknown) => void): () => void
```

The channel between your instance's two documents (main and pane), relayed
by the shell, since sandboxed siblings cannot talk directly. JSON, up to
8 KB per message; sends are dropped silently when no pane is open.

### dd.popout.flyout() [#ddpopoutflyout]

```ts
dd.popout.flyout(opts: FlyoutOpts): {
  open: (key?: string) => Promise<void>;
  close: () => void;
  toggle: (key?: string) => void;
  active: () => string | null;
}

interface FlyoutOpts {
  panes: Record<string, FlyoutPane>;
  /** The pane em base in px. Default: the anchor's computed font-size,
   *  clamped to 13..22. */
  font?: () => number;
  /** Close when the cursor leaves the widget+pane footprint. Default true. */
  retractOnLeave?: boolean;
  prefer?: "down" | "up" | "left" | "right" | "auto";
  /** Fires on every active-pane change: open, switch, close, host close. */
  onState?: (active: string | null) => void;
}

interface FlyoutPane {
  /** Pane size in CSS px for a given pane font. */
  size: (font: number) => { w: number; h: number };
  /** The element (or px rect) the pane anchors to, resolved at open time. */
  anchor: () => Element | { x: number; y: number; w: number; h: number };
  /** Extra pane data, merged over the built-in { pane, font }. */
  data?: (font: number) => Record<string, unknown>;
}
```

The transient-pane lifecycle in one helper, for the common "icon bar with
flyout panels" idiom. Give it your pane registry and it owns the whole
dance:

* mirrors host force-closes back into `onState`, so your trigger
  un-highlights;
* retracts when the cursor leaves the widget-plus-pane footprint (unless
  `retractOnLeave: false`);
* holds the retract while the pane reports a slider drag in flight (the pane
  relays `{ dragging }` via `dd.popout.send`);
* rolls back cleanly, with a warning log, when an open is refused.

```js
const flyout = dd.popout.flyout({
  panes: {
    volume: {
      size: (font) => ({ w: font * 16, h: font * 3.4 }),
      anchor: () => volumeBtn,
    },
  },
  onState: (active) => volumeBtn.toggleAttribute("active", active === "volume"),
});
volumeBtn.addEventListener("click", () => flyout.toggle("volume"));
```

## dd.peers [#ddpeers]

Needs the `peers` permission. Host-mediated drag and drop between instances
of the same widget type: the payload is captured host-side and delivered
only to the host-resolved drop target.

### dd.peers.dragStart() [#ddpeersdragstart]

```ts
dd.peers.dragStart(opts: {
  data: unknown;                                   // up to 8 KB
  ghost?: { label?: string; icon?: string };       // label 64 chars, icon a data:image/ URI up to 64 KB
}): Promise<void>
```

Starts a drag. Only callable while the primary button is physically held
inside this widget (start it from pointerdown plus movement); the gesture is
the consent. `ghost` feeds the cursor chip the shell draws. One drag exists
globally, with a 60 second timeout. The host resolves the drop on the real
button release: the payload goes only to the eligible instance under the
cursor (same widget type, visible, not the source). 20 calls per rolling 10
seconds.

### dd.peers.cancel() [#ddpeerscancel]

```ts
dd.peers.cancel(): Promise<void>
```

Abandon your own active drag (Escape, for example). No-op otherwise.

### dd.peers.onDrag() [#ddpeersondrag]

```ts
dd.peers.onDrag(cb: (state: { active: boolean; self: boolean }) => void): () => void
```

Drag lifecycle for your widget type: highlight drop targets while
`active && !self`.

### dd.peers.onDrop() [#ddpeersondrop]

```ts
dd.peers.onDrop(cb: (drop: { data: unknown; x: number; y: number }) => void): () => void
```

You are the drop target. `x` and `y` are the drop point as fractions of your
viewport. Targeted: no other frame sees the payload.

### dd.peers.onEnd() [#ddpeersonend]

```ts
dd.peers.onEnd(cb: (end: { accepted: boolean; outside: boolean }) => void): () => void
```

Your drag finished. `accepted` means a peer received it (remove the item for
move semantics). `outside` means a real drop that landed on neither a peer
nor yourself: treat as "dropped into the void" (usually remove). Both false
means snap back (released over yourself, or the host cancelled the drag).

### dd.peers.draggable() [#ddpeersdraggable]

```ts
dd.peers.draggable(el: HTMLElement, opts: {
  data: () => unknown;
  ghost?: () => { label?: string; icon?: string };
  onClick?: (e: MouseEvent) => void;
}): () => void
```

Makes an element press-draggable to peers while keeping it clickable. A
press that moves past a 6 px threshold starts the host drag (`data()` and
`ghost()` are called at that moment, and the element carries a `dd-dragging`
class until the drag ends); a plain click calls `onClick`; the phantom click
a released drag would land is swallowed for you. Returns a dispose function.
The semantic outcome still arrives via `onDrop` and `onEnd`.

### dd.peers.recentDrag() [#ddpeersrecentdrag]

```ts
dd.peers.recentDrag(): boolean
```

True briefly (350 ms) after any drag activity. Guard click handlers on
elements that are not draggable themselves; a drag released over them would
otherwise count as a click.

## dd.widgets [#ddwidgets]

Needs the `control` permission.

### dd.widgets.list() [#ddwidgetslist]

```ts
dd.widgets.list(): Promise<{ targets: WidgetTarget[] }>

interface WidgetTarget {
  instanceId: string;
  widgetId: string;
  /** The widget type's display name. */
  name: string;
  /** The target's own title setting, or "". */
  title: string;
  enabled: boolean;
  hidden: boolean;
}
```

Returns only the instances the user ticked in this widget's
`instances`-typed settings. The shell settings UI is the sole writer of
those, so a widget can never expand its own target set; manifest defaults
for `instances` fields are ignored. Self is always excluded, and stale
approvals are dropped silently. 60 calls per rolling 10 seconds.

### dd.widgets.setHidden() [#ddwidgetssethidden]

```ts
dd.widgets.setHidden(instanceId: string, hidden: boolean): Promise<void>
```

Show or hide an approved target. Unapproved ids reject `PERMISSION_DENIED`
whether or not they exist. Hidden widgets unmount entirely: they stop
running (no timers, no listeners) until shown again, from the controlling
widget or from edit mode. 30 calls per rolling 10 seconds.

A host peek is different. The user can conceal every widget at once (a
double click on empty desktop space, or Hide widgets in the tray menu);
widgets stay mounted and running, receive no input, keep their `hidden`
flag as it was, and come back the same way. `widgets.list` does not reflect
a peek and no `widgets.changed` fires for it.

### dd.widgets.onChange() [#ddwidgetsonchange]

```ts
dd.widgets.onChange(cb: () => void): () => void
```

Data-free ping when any instance's state changes (hidden, enabled, added,
removed): re-list.
