Guides

Interactivity and input

How mouse input reaches a widget: the interactive flag, capture regions marked in the DOM, hover events, and what happens when widgets overlap.

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

"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

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.

<!-- whole-panel: the entire bar is solid -->
<dd-panel id="bar" row data-dd-interactive>
  <div id="iconbar"></div>
</dd-panel>
<!-- 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

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:

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

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:
dd.cursor.onHover(({ inside }) => {
  document.body.classList.toggle("hovered", inside);
});
body.hovered #panel::part(surface) {
  border-color: var(--dd-accent);
}

There is also dd.cursor.onMove for a stream of positions while inside; see the environment reference.

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, 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

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.

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.