Every version of this project started with the same want: widgets that live on the desktop itself. Not a dashboard in a browser tab, not an always-on-top overlay nagging at the corner of the screen. Panels glued behind every application window, part of the wallpaper, there whenever you clear a space to look. I built this on pure Electron first, then properly with Win32, and then a third time when my launcher project (ZQ Engine) absorbed the technique wholesale. What follows is the distilled recipe from my build records.
The pure-Electron layer (almost the wallpaper)
Electron alone gets you surprisingly close. The window flags:
new BrowserWindow({
frame: false,
transparent: true,
hasShadow: false,
alwaysOnTop: false,
focusable: false, // -> WS_EX_NOACTIVATE
skipTaskbar: true, // tool window: no Alt-Tab, no taskbar entry
});
win.showInactive();
focusable: false maps to WS_EX_NOACTIVATE, so the window never steals focus and never auto-raises above your apps. skipTaskbar makes it a tool window, excluded from Alt-Tab. Show it with showInactive() and you have a bottom layer that sits behind everything, with zero native dependencies.
The trade-off, and the reason this post exists: it is not literally under the desktop icons. It acts like the wallpaper right up until you notice where your icons went.
WorkerW, the actual wallpaper
The real thing means Win32. WorkerW is the window layer the desktop wallpaper lives in, and if you reparent your own window into it, your app becomes the wallpaper: under every application, under the icons themselves, still standing after Win+D clears the screen.
SetParent(myHwnd, workerwHwnd) // you are now the wallpaper
Making Win32 calls from Node needs FFI (koffi, in my case), and my notes are blunt that the glue is finicky with transparent windows. The version that survived builds one spanning, opaque, focus-less window across all monitors and attaches that. Remember the opacity; it comes back at the end.
Click-through that keeps the widgets alive
A fullscreen transparent layer would swallow every click meant for the desktop icons behind it. The escape hatch is that setIgnoreMouseEvents with forward: true still delivers mousemove events to the renderer even while the window is ignoring the mouse:
// default state: clicks pass through, mousemove still arrives
win.setIgnoreMouseEvents(true, { forward: true });
On each mousemove the renderer hit-tests what is under the cursor and reports back over IPC:
const overWidget =
!!document.elementFromPoint(x, y)?.closest('.widget');
// main process, on the IPC message:
win.setIgnoreMouseEvents(!overWidget, { forward: true });
Empty desktop stays click-through; widgets are interactive. The subtlety is that click events are never forwarded, only mousemove. It works anyway because of ordering: the hover-driven toggle flips the window to interactive before your click lands.
Dragging follows the same principle of never touching the OS window. The window stays fixed and fullscreen; a "dragged" widget is a DOM card whose absolute left/top track deltas from e.screenX/Y, persisted so it survives a restart.
The furniture
An edge-reveal sidebar needs no hidden trigger window. The main process polls screen.getCursorScreenPoint() roughly every 120 ms; when the cursor reaches x >= display.bounds.right - 2 the sidebar (alwaysOnTop, non-focusable, hidden by default) reveals, and leaving the zone hides it again.
A summonable fullscreen overlay wants alwaysOnTop raised to screen-saver level, closing on Esc or a scrim click or blur, with the blur handler guarded for about 350 ms after showing so the overlay cannot close itself before focus settles.
The app is tray-resident with app.requestSingleInstanceLock(); a second launch toggles the overlay instead of spawning a duplicate. And one gotcha for start-with-Windows on an unpackaged app: app.setLoginItemSettings must be given path: process.execPath plus the app directory in args, or Windows relaunches a bare Electron with no app inside it.
The gotcha: no CORS fairy in the renderer
The widgets show live data, which means HTTP. A file:// renderer cannot read responses from http://localhost unless the server sends CORS headers, and I was not going to edit every backend I wanted a widget for. The fix is structural: all HTTP happens in the main process, which pushes results to the renderers over IPC. One poll feeds every window, and the renderers stay sandboxed (contextIsolation on, strict CSP). CORS just forces you into the better architecture early.
The Engine takes the keys
The desktop shell that pioneered all of this got retired as a standalone app, but the technique did not. When I built ZQ Engine, a kernel that launches and hosts everything I run locally, the plan was an inversion: extract the kernel, and the old shell becomes the kernel's first tenant.
The port went like this. The proven Win32 module (the WorkerW recipe plus a WH_MOUSE_LL low-level mouse hook) was copied verbatim into a host-only capabilities module and wrapped, together with the multi-monitor geometry, as two named capabilities: workerw and mouse-forward. A surface asks for them in its manifest and the host applies them. The kernel itself stays native-free, so the headless tests never touch koffi (which ships as a dependency, unpacked from the asar).
The wallpaper became a surface whose manifest reads like a personality sheet: window-hosted, target wallpaper, capabilities [workerw, mouse-forward], autostart, pause-under-fullscreen. Its host code builds the spanning opaque focus-less window, attaches it into WorkerW, installs the mouse-forward hook with a desktop-aware hit-test, and runs a heartbeat that pauses the scene under fullscreen apps and re-attaches the window if it ever gets orphaned, plus a rebuild on monitor hot-plug.
I had deliberately saved this milestone for last. It was the riskiest part of the build, and WorkerW cannot be verified headlessly; it needs a live desktop. The verification run booted the real GUI, reported the wallpaper running and attached to WorkerW, and the orphan re-attach heartbeat actually fired during the test. All 11 kernel unit tests stayed green.
One real bug fell out of that run: a handoff to the OS wallpaper that bled the actual desktop image through the transparent scene. Dropped it. (Told you the opacity would come back.)
The payoff is the widget that made the convergence worth it: a servers panel sitting on the bare desktop that reads the engine's own runnables live and can run or stop them right there, with a hotkey that raises the engine's library as a command deck on top. A later polish pass even made widgets draggable on the bare desktop through a drag-capture path in the same mouse-forward hit-test, layouts persisted per monitor setup.
None of this is exotic once it is written down, which is exactly why I write it down. The pattern entry in my records has now outlived the app it was written for, and the Win32 module it describes is on its third home, still verbatim.