# How to Create Custom Title Bars for Frameless Electron Apps on Windows and Linux

> Learn how to create custom title bars for frameless Electron apps on Windows and Linux. Implement drag regions and window controls using preload scripts and IPC.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: how-to-guide
- Published: 2026-08-21

---

**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`](https://github.com/lightningpixel/modly/blob/main/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.

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts), use `contextBridge.exposeInMainWorld` to securely inject control methods into the global `window` object.

```typescript
// 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.

```tsx
// 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">
          &#8212;
        </button>
        <button onClick={maximize} aria-label="Maximize">
          &#9633;
        </button>
        <button onClick={close} aria-label="Close">
          &#10005;
        </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`](https://github.com/lightningpixel/modly/blob/main/electron/main/ipc-handlers.ts) or within your main entry file.

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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.

```css
/* 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'`** in [`electron/main/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/index.ts) to remove the native title bar while maintaining system window management on Windows and Linux.
- **Expose methods via `contextBridge`** in [`electron/preload/electron-api.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts) to provide the renderer with controlled access to window operations through IPC.
- **Apply `-webkit-app-region: drag`** to CSS classes that should enable window movement, and `-webkit-app-region: no-drag` to interactive controls.
- **Implement IPC handlers** for `'window-minimize'`, `'window-maximize'`, `'window-unmaximize'`, and `'window-close'` events to execute corresponding `BrowserWindow` methods.
- **Reference `window.electron`** in 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.