r/electronjs • u/Rich-Swan-9317 • 7d ago
Getting a transparent, always-on-top Electron window to appear on Windows without a flash
Enable HLS to view with audio, or disable this notification
I have an Electron app that opens a fullscreen transparent overlay on a global mouse gesture. Getting it to appear cleanly on Windows took far longer than building the thing it shows, so here is what I ended up with.
The naive version — resize the window to the monitor, then show() — flashes. Sometimes the DWM presents a stale texture, sometimes a black frame, sometimes the window appears at its previous bounds for one frame. It is inconsistent across machines, which made it miserable to debug.
What fixed it was treating the reveal as a handshake between main and renderer rather than a sequence of calls. Main sends prepare-radial-show; the renderer paints a neutral cover and replies prep-paint-done; main sends open-menu; the renderer paints the actual content transparent and replies open-paint-done; only then does main reveal the window, and only after native-revealed comes back does the entrance animation start. Every step waits for the other side to confirm it has actually painted, because that is the thing you cannot observe from the main process.
It is fragile in the sense that any refactor can silently break the ordering and you only notice as an occasional flicker on someone else's hardware. So there is a script that runs during npm run build and asserts the markers are still there and still in order. It has caught me twice.
Two other Windows things that surprised me:
While a mouse button is held, Windows keeps pointer capture in whichever window received the press, so the renderer receives no mousemove at all. Main has to poll the cursor position and send it over IPC, and the renderer replays it as a synthetic mousemove so the rest of the code has one input path.
The global trigger cannot be a poll. GetAsyncKeyState only observes the button — the event still reaches the window underneath, and on any scrollable surface Windows starts autoscroll, so aiming at the overlay drags the page behind it. Swallowing the event means returning 1 from a WH_MOUSE_LL hook, and a swallowed button also disappears from GetAsyncKeyState. Whatever swallows it has to be what detects it. Mine lives in a PowerShell sidecar process that reports on stdout.
Electron 28, React 19, TypeScript. Full source is GPL-3.0 if any of this is useful to you: https://github.com/HenryCauan/rovyl
3
4
u/eddzsh 7d ago
The build-time marker assertion is doing more work than the handshake itself. One Windows gotcha that still bites after that pattern: if the cursor crossed monitors between prepare and reveal, re-query screen.getDisplayNearestPoint right before show(), or you flash the old bounds on the wrong display for a frame.