Building a frameless, draggable window in NW.js with custom controls
Step‑by‑step guide to create a frameless, draggable NW.js window with custom title‑bar controls, including manifest setup, HTML/CSS/JS, native‑module rebuild, verification and rollback.
17 Jul 2026, 08:49 UTC

Desired outcome
Create an NW.js application window that has no operating‑system title bar, can be moved by dragging a custom title‑bar area, and provides your own close/minimize/maximize buttons.
Prerequisites
- NW.js SDK version 0.52.0 or newer installed and accessible via the command line (e.g.,
nwinPATH). - A basic NW.js project folder containing at least
package.jsonand anindex.htmlfile. - If you use any Node.js native modules, have
nw-gypinstalled (npm i -g nw-gyp) and the matching NW.js headers for your target version. - Basic familiarity with HTML, CSS and JavaScript event handling.
Procedure
-
Configure the window manifest
Edit
package.jsonto include awindowsection that disables the OS frame and toolbar:{ "name": "frameless-demo", "main": "index.html", "window": { "title": "Frameless Demo", "toolbar": false, "frame": false, "width": 800, "height": 600 } }Setting
"toolbar": falseand"frame": falseremoves the default title bar, allowing you to draw your own. -
Add a custom title bar to the HTML
In
index.htmlplace a draggable div at the top of the page. Give it a distinct class so you can style it and attach listeners.<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Frameless Demo</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="titlebar" class="title-bar"> <span class="title">Frameless Demo</span> <button id="btn-close">✕</button> <button id="btn-min">−</button> <button id="btn-max">□</button> </div> <div id="content"> <p>Main application content goes here.</p> </div> </body> </html> -
Style the title bar and enable OS‑level dragging
In
style.cssapply a background, height, and crucially the-webkit-app-regionproperty to tell the OS which part of the element can start a drag operation..title-bar { -webkit-app-region: drag; /* enables native dragging when mouse is over this area */ background: #2d2d2d; color: #fff; height: 32px; display: flex; align-items: center; justify-content: space-between; padding: 0 8px; user-select: none; /* prevent text selection while dragging */ } .title-bar .title { flex: 1; text-align: center; } .title-bar button { background: transparent; border: none; color: #fff; font-size: 14px; cursor: pointer; margin-left: 4px; } #content { margin-top: 32px; /* offset for the custom bar */ padding: 16px; }The
-webkit-app-region: dragdeclaration lets the OS handle the drag when the user presses down on the title bar, so you do not need to manually move the window for simple dragging. -
Add JavaScript for custom window controls
Create a file
main.js(or embed a script) that retrieves the NW.js window object and binds click handlers to the buttons.// main.js const nwWindow = nw.Window.get(); document.getElementById('btn-close').addEventListener('click', () => { nwWindow.close(); }); document.getElementById('btn-min').addEventListener('click', () => { nwWindow.minimize(); }); document.getElementById('btn-max').addEventListener('click', () => { if (nwWindow.isMaximized()) { nwWindow.unmaximize(); } else { nwWindow.maximize(); } }); // Optional: if you prefer to implement manual dragging instead of -webkit-app-region, // uncomment the following and remove the CSS property. /* let offsetX = 0, offsetY = 0; const titleBar = document.getElementById('titlebar'); function onMouseDown(e) { offsetX = e.clientX - nwWindow.x; offsetY = e.clientY - nwWindow.y; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); } function onMouseMove(e) { nwWindow.moveTo(e.clientX - offsetX, e.clientY - offsetY); } function onMouseUp() { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); } titleBar.addEventListener('mousedown', onMouseDown); */Include the script in
index.html:<script src="main.js"></script> -
Rebuild native modules (if any)
If your project depends on native Node.js modules, rebuild them against the NW.js version you are using to avoid ABI mismatches that could crash the window code.
# From your project root nw-gyp rebuild --target=0.52.0 --arch=x64Replace
0.52.0with the exact NW.js version you target. -
Run and verify
Launch the app with the NW.js binary:
nw .Check the following:
- The window appears without an OS title bar.
- Clicking and holding anywhere on the dark title bar moves the window smoothly.
- The close, minimize and maximize buttons work as expected.
- Open DevTools (F12 or Ctrl+Shift+I) and confirm there are no console errors, especially no
ERR_DLOPEN_FAILEDor similar messages indicating native module problems.
Expected checks
- Visual: No native title bar; custom bar visible.
- Functional: Drag moves window; buttons control window state.
- Diagnostic: DevTools console clean; network tab shows no failed loads.
Recovery options (rollback)
If the changes cause the application to fail to start:
- Manifest: Restore the previous
package.jsonwindow section (or remove"toolbar": falseand"frame": false) to revert to the default framed window. - HTML/CSS/JS: Remove the custom title bar div and associated scripts/styles, or comment them out.
- Native modules: Reinstall the original modules built for Node.js (e.g.,
npm rebuild) or delete thebuilddirectory and let npm reinstall pre‑built binaries. - After each rollback step, run
nw .again to confirm the app returns to its prior working state.
Limitations
- Disabling the frame removes standard window controls; you must provide your own UI for close, minimize, maximize, and optionally for window shading or roll‑up.
- If you rely on
-webkit-app-region: drag, ensure that interactive elements (buttons, inputs) inside the title bar do not unintentionally trigger drags; cover them with-webkit-app-region: no-dragif needed. - High‑DPI displays may require scaling adjustments; test on each target OS (Windows, macOS, Linux) and adjust CSS pixel values or use
devicePixelRatioto position elements correctly. - Manual dragging via
moveBy(if you choose not to use the CSS property) can introduce jitter under rapid mousemove events; throttling withrequestAnimationFrameor a debounce interval is recommended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.