Multi-platform Electron window drag tool
Install the electron-draggable package via npm:
npm install electron-draggableThe electron-draggable package provides a lightweight, main-process solution for enabling window dragging in frameless Electron windows. It hooks into the webContents.on('before-mouse-event') to intercept mouse events and move the window accordingly, without requiring any CSS (-webkit-app-region) or renderer-process code.
- Zero renderer-side code — everything runs in the main process.
- Works with
BrowserWindowandBaseWindow— supports both standard and multi-view setups. - Selector-based filtering — include or exclude specific elements from dragging via CSS selectors.
- Drag region — restrict dragging to a specific region (e.g., a custom title bar) using partial bounds.
- Double-click to maximize — optional built-in support for maximize/unmaximize on double-click.
- Configurable FPS — drag update rate defaults to the screen refresh rate.
All public methods are documented with JSDoc and can be referenced during import.
To import the Draggable class:
const { Draggable } = require('electron-draggable')The from static method returns a single cached instance per window. If an instance already exists for the window, it is returned immediately:
const drag = Draggable.from(window)You can also pass drag options during creation:
const drag = Draggable.from(window, { region: { height: 40 }, maximize: true })When a BrowserWindow is used, its webContents is automatically attached as a drag source (unless attachOnInit is set to false).
Use create when you need multiple independent drag managers for the same window:
const drag = Draggable.create(window, options)Each instance independently manages its own drag configuration and WebContents attachments. The instance is not cached on the window object.
| Option | Type | Default | Description |
|---|---|---|---|
region |
Partial<Rectangle> |
Entire Window | Restricts dragging to a region. Only specified bounds are checked (e.g., { height: 40 } for the top 40px). |
selector |
string |
Disabled | CSS selector that marks elements as drag handles. Can be combined with exclude. |
exclude |
string |
Disabled | CSS selector for elements that should NOT trigger drag. Can be combined with selector. |
button |
'left' | 'right' | 'middle' |
'left' |
Mouse button that triggers the drag. |
maximize |
boolean |
Disabled | Enable double-click to maximize/unmaximize. |
fps |
number |
Screen refresh rate | Frames per second for drag position updates. |
attachOnInit |
boolean |
Enabled | If true, auto-attach the BrowserWindow.webContents on initialization. |
Note:
selectorandexcludecan be used together. Useselectorto whitelist draggable areas,excludeto blacklist non-draggable elements, or both to whitelist a region and carve out exceptions within it.
The button option specifies which mouse button initiates the drag. Accepted values are 'left', 'right', and 'middle'.
// Drag with right-click (useful when left-click is reserved for other interactions)
Draggable.from(window, { button: 'right' })
// Drag with middle-click
Draggable.from(window, { button: 'middle' })The region option defines draggable bounds using up to 4 independent thresholds. Only specified attributes are checked — omitted ones impose no constraint:
| Attribute | Constraint | Default |
|---|---|---|
x |
Drag only when cursor is at or after x px from the left |
0 (no left bound) |
y |
Drag only when cursor is at or after y px from the top |
0 (no top bound) |
width |
Drag only within width px after x |
∞ (no right bound) |
height |
Drag only within height px after y |
∞ (no bottom bound) |
x x + width
┬ ┬
┌──────┼────────┼──────┐
│ 0,0 │ │ │
│ │ │ │
│ ░░░░░░░░░░──────┼─┤ y
│ ░░ DRAG ░░ │
│ ░░ ZONE ░░ │
│ ░░░░░░░░░░──────┼─┤ y + height
│ │
│ │
└──────────────────────┘
Examples:
// Title bar: top 40px
{ height: 40 }
// Sidebar handle: left 8px
{ width: 8 }
// Custom band: between 50px and 150px from top
{ y: 50, height: 100 }
// Scoped box: specific region
{ x: 100, y: 50, width: 300, height: 150 }
// Entire content
/* empty */For BaseWindow setups with multiple WebContentsView children, you can manually attach and detach drag sources:
const drag = Draggable.from(baseWindow)
// Attach a view's webContents with per-view overrides
drag.attach(view.webContents, { exclude: '.no-drag, button' })
// Detach when no longer needed
drag.detach(view.webContents)
// Or detach all at once
drag.detachAll()Each attached WebContents can have its own override options that are merged with the instance defaults.
You can update options at any time. Changes apply immediately to all subsequent drag interactions:
// Update options for ALL attached WebContents
drag.updateOptions({ fps: 120, maximize: true })
// Update options for a SPECIFIC WebContents
drag.updateOptions(view.webContents, { region: { height: 60 } })To permanently disable drag and release all references:
drag.destroy()After calling destroy, the instance is dead and must not be reused. The drag is also automatically disabled when the window is closed.
If you need to move the drag instance to a new window:
drag.setWindow(newWindow)This clears the old window reference and sets the new one, preserving all attached WebContents and their configurations.
const { app, BrowserWindow } = require('electron')
const { Draggable } = require('electron-draggable')
app.whenReady().then(() => {
const window = new BrowserWindow({ frame: false })
window.loadURL('https://github.com/ECRomaneli/electron-draggable')
// Enable dragging on the top 40px, with double-click to maximize
Draggable.from(window, { region: { height: 40 }, maximize: true })
})const { app, BaseWindow, WebContentsView } = require('electron')
const { Draggable } = require('electron-draggable')
app.whenReady().then(() => {
const window = new BaseWindow({ width: 800, height: 600, frame: false })
const view = new WebContentsView()
window.contentView.addChildView(view)
view.setBounds({ x: 0, y: 0, width: 800, height: 600 })
view.webContents.loadFile('index.html')
// Create drag instance and attach the view
const drag = Draggable.from(window, { region: { height: 100 } })
drag.attach(view.webContents, { exclude: '.no-drag, button' })
})- The
Draggableinstance hooks intowebContents.on('before-mouse-event')for each attachedWebContents. - On
mouseDown, it checks whether the click position is within the drag region and matches the selector/exclude rules (usingdocument.elementFromPointviaexecuteJavaScript). - If the position is draggable, it records the offset between the cursor and the window position.
- On
mouseMove, it starts an interval (at the configured FPS) that reads the cursor screen position and updates the window position accordingly. - On
mouseUp(or any non-move event), the interval is cleared and dragging stops. - If
maximizeis enabled and a double-click is detected, the window is toggled between maximized and normal states.
/**
* Get or create a Draggable instance for the given window.
* Returns a single cached instance per window.
* @param {BaseWindow} window - The Electron window to enable dragging for.
* @param {DragOptions} [options] - Optional drag behavior configuration.
* @returns {Draggable} A unique Draggable instance for this window.
*/
Draggable.from(window, options)
/**
* Create a new independent Draggable instance for the given window.
* The instance is NOT cached on the window object.
* @param {BaseWindow} window - The Electron window to enable dragging for.
* @param {DragOptions} [options] - Optional drag behavior configuration.
* @returns {Draggable} A new independent Draggable instance.
*/
Draggable.create(window, options)/**
* Register a WebContents as a drag source for this window.
* @param {WebContents} webContents - The WebContents to attach.
* @param {DragOptions} [overrideOptions] - Optional per-WebContents drag options.
*/
attach(webContents, overrideOptions)
/**
* Unregister a WebContents from dragging.
* @param {WebContents} webContents - The WebContents to detach.
*/
detach(webContents)
/**
* Unregister all WebContents from dragging.
*/
detachAll()
/**
* Disable drag for all registered WebContents and release the window reference.
* After calling this method, the instance is dead and must not be reused.
*/
destroy()
/**
* Update drag options for all registered WebContents.
* @param {Partial<DragOptions>} update - Partial options to merge.
*/
updateOptions(update)
/**
* Update drag options for a specific WebContents.
* @param {WebContents} webContents - The WebContents to update options for.
* @param {Partial<DragOptions>} update - Partial options to merge.
*/
updateOptions(webContents, update)
/**
* Retarget the instance to a new window.
* @param {BaseWindow} newWindow - The new window to attach to.
*/
setWindow(newWindow)Created by Emerson Capuchi Romaneli (@ECRomaneli).
This project is licensed under the MIT License.
