Skip to content

Draggable Regions

Electrobun’s injected preload turns designated DOM regions into native window drag handles. No Electroview instance or RPC setup is required for dragging itself.

Use the compatibility classes:

<header class="titlebar electrobun-webkit-app-region-drag">
<strong>My App</strong>
<nav class="window-controls electrobun-webkit-app-region-no-drag">
<button id="minimize" type="button" aria-label="Minimize">_</button>
<button id="maximize" type="button" aria-label="Maximize">+</button>
<button id="close" type="button" aria-label="Close">x</button>
</nav>
</header>

Or use an app-region CSS property. Electrobun recognizes -webkit-app-region, app-region, and the legacy window-drag spelling in inline and same-origin stylesheets.

.titlebar {
-webkit-app-region: drag;
app-region: drag;
align-items: center;
display: flex;
height: 36px;
user-select: none;
}
.window-controls,
.window-controls button {
-webkit-app-region: no-drag;
app-region: no-drag;
}

A no-drag descendant always wins over a draggable ancestor. Apply it to buttons, inputs, links, menus, and every other interactive control inside the titlebar.

Window Configuration

Use titleBarStyle: "hidden" for fully custom chrome. hiddenInset retains the inset native controls on macOS.

import { BrowserWindow } from "electrobun/main";
const win = new BrowserWindow({
title: "Custom Titlebar",
url: "views://mainview/index.html",
frame: { width: 800, height: 600 },
titleBarStyle: "hidden",
});
void win;

Custom Window Controls

Custom HTML buttons need typed RPC to invoke native window operations. Define the same schema in both contexts. In a project, place this type in a shared module rather than duplicating it.

Main process:

import {
BrowserView,
BrowserWindow,
type RPCSchema,
} from "electrobun/main";
type WindowControlsRPC = {
bun: RPCSchema<{
requests: {};
messages: {
closeWindow: void;
minimizeWindow: void;
maximizeWindow: void;
};
}>;
webview: RPCSchema<{ requests: {}; messages: {} }>;
};
let win: BrowserWindow;
const rpc = BrowserView.defineRPC<WindowControlsRPC>({
handlers: {
requests: {},
messages: {
closeWindow: () => win.close(),
minimizeWindow: () => win.minimize(),
maximizeWindow: () => {
if (win.isMaximized()) win.unmaximize();
else win.maximize();
},
},
},
});
win = new BrowserWindow({
title: "Custom Titlebar",
url: "views://mainview/index.html",
frame: { width: 800, height: 600 },
titleBarStyle: "hidden",
rpc,
});

Browser view:

import { Electroview, type RPCSchema } from "electrobun/view";
type WindowControlsRPC = {
bun: RPCSchema<{
requests: {};
messages: {
closeWindow: void;
minimizeWindow: void;
maximizeWindow: void;
};
}>;
webview: RPCSchema<{ requests: {}; messages: {} }>;
};
const rpc = Electroview.defineRPC<WindowControlsRPC>({
handlers: { requests: {}, messages: {} },
});
new Electroview({ rpc });
document.querySelector("#close")?.addEventListener("click", () => {
rpc.send.closeWindow();
});
document.querySelector("#minimize")?.addEventListener("click", () => {
rpc.send.minimizeWindow();
});
document.querySelector("#maximize")?.addEventListener("click", () => {
rpc.send.maximizeWindow();
});

The RPC calls use the local rpc object returned by defineRPC. Constructing Electroview attaches its transport to that object.

Stylesheet Notes

Electrobun mirrors app-region declarations into a custom property so system webviews that discard the vendor property still preserve CSS cascade. Inline styles and same-origin stylesheets are supported. A cross-origin stylesheet that cannot be fetched may depend on what the current renderer exposes through computed style; keep titlebar rules in application-owned CSS.