(this article requires you have some good understanding of how electron works)
Sad story: I wanted my app to be fully draggable and still change its height and width on mouse enter and mouse leave. But you can’t have both out of the box—making a window draggable requires ignoring mouse events in the drag area, and I want almost the entire window to act as the drag space. This means normal enter/leave detection breaks immediately.
Here is how I configured the window to look sleek like that :
const mainWindow = new BrowserWindow({
// the usual width , height ,x , y .. etc
titleBarStyle: 'hidden',
backgroundMaterial:"acrylic",
visualEffectState:"active",
vibrancy: "popover", // on MacOS
frame: false, // make it frame less
});
and to make it dragable you just add this class to the areas you want to make dragAble:
.dragAble {
-webkit-user-select: none;
-webkit-app-region: drag;
}
for the element that you still want to keep intractable , add this class :
.no-drag {
-webkit-app-region: no-drag;
}
*Alright now lets see how we got to the happy ending : *
how did I handle mouseEnter and mouseLeave while still making window dragAble ?
Electron doesn’t provide native mouse-enter or mouse-leave events, so I built them myself using two steps:
A polling listener: A utility function runs on an interval, checks the cursor position against the window bounds, and determines whether the mouse is inside or outside. It then calls the provided callback with inside.
Handling the result: When using this listener, we pass a callback that reacts to the inside/outside state and triggers our custom enter/leave logic.
Here’s the small utility that acts as the “mouse listener”:
function listenToMouseMovement(callback) {
const id = setInterval(() => {
const cursor = screen.getCursorScreenPoint();
const bounds = esm.mainWindow.getBounds();
const inside =
cursor.x >= bounds.x &&
cursor.x <= bounds.x + bounds.width &&
cursor.y >= bounds.y &&
cursor.y <= bounds.y + bounds.height;
callback({
inside,
position: cursor,
});
}, 8); // ~60fps polling
}
We listen to mouse movement and pass a callback. Electron can fire “inside” and “outside” states very quickly when the cursor sits near the window’s edge, which normally causes a flicker. In my case it’s even worse because the app changes its dimensions on mouse enter and leave, so those rapid state switches create constant resizing flickers.
To fix this, we debounce both events:
When the mouse goes inside, we cancel any pending “leave” timeout and delay the “enter” event just enough to confirm the cursor really came back in.
When the mouse goes outside, we cancel any pending “enter” timeout and delay the “leave” event to confirm it’s truly outside and not immediately returning.
This small delay prevents false triggers and stops the UI from resizing back and forth near the window edge.
listenToMouseMovement(({ inside }) => {
if (esm.isMovingWindow) return; // more on this below
if (inside) {
// cancel any pending leave event
if (leaveTimeout) {
clearTimeout(leaveTimeout);
leaveTimeout = null;
}
if (esm.mouseWasOutsideWindow) {
// debounce the enter event
if (!enterTimeout) {
enterTimeout = setTimeout(() => {
esm.mouseWasOutsideWindow = false;
// ask renderer to show mouse outside UI
mainWindow.webContents.send("onMouseIsInsideTheApp");
}
}
}
else {
// cancel any pending enter event
if (enterTimeout) {
clearTimeout(enterTimeout);
enterTimeout = null;
}
if (!esm.mouseWasOutsideWindow) {
// debounce the leave event
if (!leaveTimeout) {
leaveTimeout = setTimeout(() => {
esm.mouseWasOutsideWindow = true;
mainWindow.webContents.send("onMouseIsOutsideTheApp");
}, 120);
}
}
}
});
The mouse listener alone isn’t enough, because we don’t want to trigger enter/leave checks while the window is being dragged. To handle that, we listen to Electron’s native "will-move", "move", and "moved" events and track a small state flag. When the window starts moving, we set isMovingWindow = true ( we used this in listenToMouseMovement ), and once movement stops, we wait briefly before setting it back to false to avoid false detections.
esm.mainWindow.on("will-move", () => {
esm.isMovingWindow = true;
});
esm.mainWindow.on("move", () => {
esm.isMovingWindow = true;
if (esm.moveEndTimeout) {
clearTimeout(esm.moveEndTimeout);
esm.moveEndTimeout = null;
}
});
esm.mainWindow.on("moved", () => {
if (esm.moveEndTimeout) clearTimeout(esm.moveEndTimeout);
esm.moveEndTimeout = setTimeout(() => {
esm.isMovingWindow = false;
esm.moveEndTimeout = null;
}, 200);
});
you can check the result in this tweet :
Thanks for reading , this is my first ever article and there is more to come .


Top comments (0)