How to Create Custom Title Bars for Frameless Electron Apps on Windows and Linux
To implement custom title bars in frameless Electron applications, configure titleBarStyle: 'hidden' in the main process, expose window control methods through a context-isolated preload script, and apply -webkit-app-region: drag CSS to enable draggable regions while routing button clicks via IPC channels.
The lightningpixel/modly repository demonstrates a production-ready pattern for replacing native window frames with custom React components. By combining Electron's frameless window options with secure IPC communication, you can build branded title bars that maintain full window management capabilities across Windows and Linux platforms.
Configure Frameless Windows in the Main Process
Begin by instantiating the BrowserWindow in electron/main/index.ts with the titleBarStyle option set to 'hidden'. This removes the operating system title bar while preserving native resize handles and window management.
// electron/main/index.ts
import { BrowserWindow } from 'electron';
import { join } from 'path';
const createWindow = () => {
const win = new BrowserWindow({
width: 1200,
height: 800,
titleBarStyle: 'hidden', // Removes native title bar on Windows/Linux
webPreferences: {
preload: join(__dirname, '../preload/electron-api.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
return win;
};
On Windows, this setting eliminates the standard title bar and menu bar. On Linux, Electron automatically treats this as a frameless window, though you may explicitly set frame: false for specific desktop environment compatibility.
Expose Safe Window Controls via Preload
Since the renderer runs in a sandboxed context, you must bridge window operations through a preload script. In electron/preload/electron-api.ts, use contextBridge.exposeInMainWorld to securely inject control methods into the global window object.
// electron/preload/electron-api.ts
import { contextBridge, ipcRenderer } from 'electron';
export const createElectronApi = () => ({
windowControls: {
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
unmaximize: () => ipcRenderer.send('window-unmaximize'),
close: () => ipcRenderer.send('window-close'),
},
});
contextBridge.exposeInMainWorld('electron', createElectronApi());
This approach maintains process isolation by allowing the renderer to send IPC messages without granting direct access to Node.js or Electron main process APIs.
Implement the Custom Title Bar Component
Create a React component that renders your custom interface. The component invokes the exposed window.electron methods when users interact with window controls.
// src/components/CustomTitleBar.tsx
import React from 'react';
import './CustomTitleBar.css';
export const CustomTitleBar = () => {
const minimize = () => window.electron?.windowControls?.minimize();
const maximize = () => window.electron?.windowControls?.maximize();
const close = () => window.electron?.windowControls?.close();
return (
<div className="title-bar">
<div className="drag-region" />
<div className="window-controls">
<button onClick={minimize} aria-label="Minimize">
—
</button>
<button onClick={maximize} aria-label="Maximize">
□
</button>
<button onClick={close} aria-label="Close">
✕
</button>
</div>
</div>
);
};
The component relies on CSS-based draggable regions to handle window movement, delegating all state changes to the main process via IPC.
Handle IPC Messages in the Main Process
Register listeners in the main process to execute actual window operations when the renderer sends control signals. Create these handlers in electron/main/ipc-handlers.ts or within your main entry file.
// electron/main/ipc-handlers.ts
import { ipcMain, BrowserWindow } from 'electron';
export const registerWindowHandlers = (mainWindow: BrowserWindow) => {
ipcMain.on('window-minimize', () => mainWindow.minimize());
ipcMain.on('window-maximize', () => mainWindow.maximize());
ipcMain.on('window-unmaximize', () => mainWindow.unmaximize());
ipcMain.on('window-close', () => mainWindow.close());
};
Import and execute this registration function immediately after creating the BrowserWindow instance in electron/main/index.ts to ensure handlers are active before the renderer loads.
Style Draggable Regions with CSS
Use the -webkit-app-region property to define which areas trigger window dragging. Apply drag to background regions and no-drag to interactive elements.
/* src/components/CustomTitleBar.css */
.title-bar {
display: flex;
align-items: center;
height: 32px;
background: #2d2d2d;
user-select: none;
}
.drag-region {
flex: 1;
height: 100%;
-webkit-app-region: drag; /* Enables window dragging from this area */
}
.window-controls {
display: flex;
height: 100%;
}
.window-controls button {
-webkit-app-region: no-drag; /* Prevents drag behavior on buttons */
background: transparent;
border: none;
color: #ffffff;
padding: 0 16px;
cursor: pointer;
font-size: 14px;
}
.window-controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
The flex: 1 declaration on .drag-region ensures that all space between the window edge and control buttons remains draggable, while the buttons themselves respond to click events.
Summary
- Set
titleBarStyle: 'hidden'inelectron/main/index.tsto remove the native title bar while maintaining system window management on Windows and Linux. - Expose methods via
contextBridgeinelectron/preload/electron-api.tsto provide the renderer with controlled access to window operations through IPC. - Apply
-webkit-app-region: dragto CSS classes that should enable window movement, and-webkit-app-region: no-dragto interactive controls. - Implement IPC handlers for
'window-minimize','window-maximize','window-unmaximize', and'window-close'events to execute correspondingBrowserWindowmethods. - Reference
window.electronin your React components to trigger window state changes from the custom title bar interface.
Frequently Asked Questions
What is the difference between titleBarStyle: 'hidden' and frame: false?
titleBarStyle: 'hidden' removes the title bar but preserves the native window frame and resize handles on Windows and Linux, ensuring proper system integration. frame: false removes the entire window frame, requiring manual implementation of resize regions and shadows. The modly implementation uses titleBarStyle: 'hidden' to maintain native window behaviors without the standard title bar.
Why are my custom title bar buttons not responding to click events?
If buttons appear frozen or unresponsive, they likely inherit draggable region behavior. Verify that your button CSS includes -webkit-app-region: no-drag to exclude them from the drag region. Additionally, confirm that contextIsolation is enabled in your webPreferences and that the preload script correctly exposed the windowControls object to window.electron.
How do I make only a specific portion of the title bar draggable?
Apply -webkit-app-region: drag selectively to container elements rather than the entire title bar. For instance, you might restrict dragging to a centered logo area or the space between the application title and the window controls, leaving margins available for toolbars or tabs.
Does this custom title bar approach work on macOS?
While titleBarStyle: 'hidden' functions on macOS, it preserves the traffic light buttons in the top-left corner rather than removing all chrome. The IPC-based custom controls described here target Windows and Linux specifically. macOS implementations typically require different CSS strategies, such as titleBarStyle: 'hiddenInset', to accommodate the platform's native window management conventions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →