How I Made an Electron Window Draggable and Kept Mouse Enter/Leave Detection

Published: (December 3, 2025 at 07:41 AM EST)
2 min read
Source: Dev.to

Source: Dev.to

Introduction

I wanted my app to be fully draggable while still changing its height and width on mouse enter and mouse leave. By default, making a window draggable ignores mouse events in the drag area, which breaks normal enter/leave detection.

Visual comparison

Mouse outside

Focus app widget mouse outside

Mouse inside

Focus app widget mouse inside

Window configuration

const mainWindow = new BrowserWindow({
  // width, height, x, y, etc.
  titleBarStyle: 'hidden',
  backgroundMaterial: "acrylic",
  visualEffectState: "active",
  vibrancy: "popover", // macOS
  frame: false, // frameless window
});

Making the window draggable

Add the following class to any area that should act as a drag region:

.dragAble {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}

For elements that must remain interactive, use:

.no-drag {
  -webkit-app-region: no-drag;
}

Handling mouse‑enter and mouse‑leave

Electron does not provide native mouse‑enter/leave events for the whole window, so we implement them ourselves.

1. Polling listener

A utility runs on an interval, checks the cursor position against the window bounds, and calls a callback with the result.

function listenToMouseMovement(callback) {
  const id = setInterval(() => {
    const cursor = screen.getCursorScreenPoint();
    const bounds = esm.mainWindow.getBounds();

    const inside =
      cursor.x >= bounds.x &&
      cursor.x = bounds.y &&
      cursor.y  {
  if (esm.isMovingWindow) return; // ignore while dragging

  if (inside) {
    // Cancel any pending leave timeout
    if (leaveTimeout) {
      clearTimeout(leaveTimeout);
      leaveTimeout = null;
    }

    if (esm.mouseWasOutsideWindow) {
      // Debounce the enter event
      if (!enterTimeout) {
        enterTimeout = setTimeout(() => {
          esm.mouseWasOutsideWindow = false;
          mainWindow.webContents.send("onMouseIsInsideTheApp");
        }, 120);
      }
    }
  } else {
    // Cancel any pending enter timeout
    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);
      }
    }
  }
});

3. Ignoring mouse checks while the window is being moved

We track the window’s moving state using Electron’s native events:

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);
});

Result

The approach eliminates flickering and allows the window to be fully draggable while still reacting to mouse‑enter and mouse‑leave events. You can see the behavior in action in this tweet: (link omitted for brevity)

Thanks for reading! This is my first article, and more is on the way.

Back to Blog

Related posts

Read more »