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": trueWith 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-hoveron an element: the SDK toggles add-hoverclass on it while the cursor is over it. Pure styling, no subscription.dd.cursor.onHoverfor 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: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
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.
Gestures and guards
- A press-drag that may leave your marked regions (a slider, a drag
handle) should take
setPointerCaptureon pointerdown, so the gesture survives leaving the rect. If the drag is a peer drag,dd.peers.draggablealready 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. A widget can opt
out with
"contextMenu": falsein its manifest (needs"minAppVersion": "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.
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> 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
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.