Electron 自定义标题栏实战指南:titleBarStyle、Window Controls Overlay 与跨平台拖拽实现
在本仓库的 custom-title-bar.md 教程中,Electron 官方详细演示了如何移除操作系统默认的 window chrome,并用 HTML/CSS 构建自己的标题栏,同时让窗口在 Windows、Linux 与 macOS 上保持可拖拽、可控制的原生体验。读完本文你将掌握:titleBarStyle 与 titleBarOverlay 的全部配置语义、app-region: drag 拖拽区域的正确用法、env(titlebar-area-*) 安全区的适配技巧,以及 macOS 交通灯按钮(traffic lights)的隐藏、位移与自定义方案。
什么是 window chrome,为什么要自定义标题栏
Electron 文档中使用的 chrome 一词并非指 Google Chrome 浏览器,而是指窗口框架中不属于网页主体内容的那部分装饰元素,包括标题栏、工具栏与窗口控制按钮。默认情况下,操作系统会为应用窗口绘制一套原生 chrome,它对简单应用足够好用,但不同平台(macOS / Windows / Linux)的观感差异很大,很难与应用自身的 UI 风格统一。
因此很多应用会选择移除系统标题栏,再用 Web 技术重绘一套跨平台观感一致的标题栏。这正是本教程围绕 BrowserWindow 的窗口配置选项展开的核心诉求。BrowserWindow 是 BaseWindow 的子类,二者的构造选项用法可以互通,教程中的示例同样适用于 BaseWindow,详见 base-window.md。
本仓库在 docs/fiddles/features/window-customization/custom-title-bar/ 下提供了与教程逐步骤对应的可运行 Fiddle 示例,共五个阶段,我们按顺序逐个实现:
| 步骤目录 | 作用 |
|---|---|
starter-code/ |
最原始的默认窗口,作为动手起点 |
remove-title-bar/ |
隐藏默认标题栏 |
native-window-controls/ |
Windows / Linux 恢复原生窗口控制按钮 |
custom-title-bar/ |
用 HTML/CSS 绘制自定义标题栏 |
custom-drag-region/ |
让标题栏区域可拖拽移动窗口 |
safe-area/ |
避开原生窗口控制的安全区适配 |
第一步:移除默认标题栏
教程的第一步从 starter-code/main.js 开始,它只是创建了一个空配置的 BrowserWindow 并加载远程页面:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({})
win.loadURL('https://example.com')
}
app.whenReady().then(() => {
createWindow()
})
要移除默认标题栏,只需在构造器中把 titleBarStyle 设为 'hidden',这是 base-window-options.md 中定义的窗口选项之一:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
// remove the default titlebar
titleBarStyle: 'hidden'
})
win.loadURL('https://example.com')
}
app.whenReady().then(() => {
createWindow()
})
完整代码见 remove-title-bar/main.js。
这里必须强调 hidden 值在不同平台上的行为差异(源码依据见 base-window-options.md 中 titleBarStyle 的逐值说明):
- macOS:标题栏被隐藏,网页内容撑满整个窗口,但左上角的原生"交通灯"关闭/最小化/最大化按钮仍然保留并可点击;
- Windows / Linux:标题栏被隐藏后,如果不配合
titleBarOverlay: true,任何窗口控制按钮都不会显示,你需要在下一节自行恢复它们; - 作为对照,
default(默认值)在各平台呈现标准原生标题栏。
第二步:在 Windows / Linux 恢复原生窗口控制
由于 macOS 在 hidden 下仍保留交通灯按钮,需要额外处理窗口控制的只有 Windows 和 Linux。做法是在构造选项中设置 titleBarOverlay,让系统把最小化/最大化/关闭按钮重新"浮"回窗口右上角(或根据系统设置/ RTL 布局位于左侧)。
Fiddle 示例 native-window-controls/main.js 展示了推荐的平台条件写法——通过展开运算符只在非 macOS 平台注入该选项,避免影响 macOS 的交通灯行为:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
// remove the default titlebar
titleBarStyle: 'hidden',
// expose window controls in Windows/Linux
...(process.platform !== 'darwin' ? { titleBarOverlay: true } : {})
})
win.loadURL('https://example.com')
}
app.whenReady().then(() => {
createWindow()
})
titleBarOverlay: true 是让窗口控制恢复显示的最简写法,此时按钮会使用系统默认配色与默认高度。如果你希望进一步自定义按钮高度与颜色,参见后文"[通过 Window Controls Overlay 定制窗口控制]"(#通过-window-controls-overlay-定制窗口控制)小节。
从构造选项文档 base-window-options.md 可以确认一个重要前提:titleBarOverlay 要求 titleBarStyle 的值不能是 default(即必须是 hidden、hiddenInset、customButtonsOnHover 等隐藏标题栏方案),否则该特性不生效。
第三步:用 HTML/CSS 画一个自定义标题栏
接下来不再依赖系统,改为在 webContents 里手绘标题栏。教程 custom-title-bar 阶段 的窗口逻辑与上一步一致,只是把加载内容换成同目录下的 index.html:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
// remove the default titlebar
titleBarStyle: 'hidden',
// expose window controls in Windows/Linux
...(process.platform !== 'darwin' ? { titleBarOverlay: true } : {})
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
HTML 的结构非常简单,只需把标题栏挂载在 body 最顶部即可(完整代码见 custom-title-bar/index.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<link href="./styles.css" rel="stylesheet">
<title>Custom Titlebar App</title>
</head>
<body>
<!-- mount your title bar at the top of you application's body tag -->
<div class="titlebar">Cool titlebar</div>
</body>
</html>
样式上"没有什么花哨的东西,只是 HTML 和 CSS"(见 custom-title-bar/styles.css):
body {
margin: 0;
}
.titlebar {
height: 30px;
background: blue;
color: white;
display: flex;
justify-content: center;
align-items: center;
}
注意 HTML 中带上了
Content-Security-Policy元信息,它限定页面只能加载自身资源,是 Electron 官方示例的标准安全实践;内联样式需要style-src 'self' 'unsafe-inline'才能正常生效。
第四步:让自定义标题栏能拖动窗口
此时窗口虽然显示为自定义标题栏,但窗口无法被鼠标拖动——因为系统标题栏已被移除,没有任何区域告诉操作系统"这里可以被按住拖动"。解决办法是在自定义标题栏的 CSS 上追加 app-region: drag:
body {
margin: 0;
}
.titlebar {
height: 30px;
background: blue;
color: white;
display: flex;
justify-content: center;
align-items: center;
app-region: drag;
}
对应 Fiddle 见 custom-drag-region/styles.css,窗口逻辑与 HTML 结构同上一步保持一致(custom-drag-region/main.js 与 custom-drag-region/index.html)。
这里必须补充一个极易踩坑的交互细节:drag 会吞掉其子元素的鼠标事件。如果标题栏里还有按钮、输入框、搜索框这类需要点击的交互控件(例如 macOS 风格的自定义交通灯、Windows 风格的应用菜单按钮),必须在这些子元素上显式设置 app-region: no-drag,否则它们将无法被点击。关于拖拽区域与可交互区域的完整管理方法,请继续阅读同目录下的 custom-window-interactions.md("Custom draggable regions"小节)。
第五步:用 env(titlebar-area-*) 避开原生窗口控制
还差最后一步:我们自定义的标题栏内容可能与浮在窗口上的原生窗口控制按钮(Windows/Linux 的右上角关闭/最小化按钮,或 RTL 与用户设置导致位于左侧)互相重叠。由于按钮可能出现在左侧、右侧甚至两侧,靠写死 margin 并不可靠。
Electron 提供的解法是使用一组 CSS 环境变量,让标题栏自动收缩到"不会被窗口控制遮住"的安全区:
env(titlebar-area-x, 0px):安全区相对于窗口的横向偏移;env(titlebar-area-width, 100%):安全区可用宽度;env(titlebar-area-height, 30px):安全区高度。
教程 safe-area 阶段 给出的完整写法如下(用虚线边框直观标出安全区边界):
body {
margin: 0;
}
.titlebar {
background: blue;
color: white;
display: flex;
justify-content: center;
align-items: center;
app-region: drag;
margin-left: env(titlebar-area-x, 0);
width: env(titlebar-area-width, 100%);
height: env(titlebar-area-height, 30px);
box-sizing: border-box;
border: 1px dashed red;
}
各环境变量后面的参数(如 0、100%、30px)是回退值:当页面未处于 Window Controls Overlay 模式、变量不可用时,标题栏会退回到"占满整行、高度 30px"的普通布局,从而保证不启用 overlay 的平台(如 macOS)也能正常渲染。配套窗口逻辑与 HTML 分别见 safe-area/main.js 与 safe-area/index.html。
到这里,一个"可拖动、不与原生按钮重叠"的基础自定义标题栏就完成了。仓库中的测试 fixture pages/overlay.html 也使用了 titlebar-area-* 变量,可作为如何在实际页面中消费这些变量的补充参考。
进阶定制(macOS):交通灯按钮的显示与位置
macOS 用户不满足于默认的左上角原生按钮时,Electron 提供了从"显示时机"到"精确坐标"的完整控制链。
悬停才显示的 customButtonsOnHover
如果你打算在 HTML 中绘制自己的交通灯按钮,但仍希望用原生 UI 真正控制窗口,可以使用 customButtonsOnHover 风格:标题栏隐藏,原生交通灯平时不显示,只有当鼠标悬停到窗口左上角区域时才浮现出来。注意 base-window-options.md 中标注该选项目前是实验性的:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ titleBarStyle: 'customButtonsOnHover' })
调整交通灯的位置
位置控制有两个层级。若只需整体下移/内缩一段固定距离,使用 hiddenInset 风格,它会以固定量调整交通灯的纵向内边距(macOS 专用):
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ titleBarStyle: 'hiddenInset' })
如果需要更精细的定位,可以通过 trafficLightPosition 直接传入一组坐标(类型为 Point,即 { x, y } 像素值),通常配合 titleBarStyle: 'hidden' 使用:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
titleBarStyle: 'hidden',
trafficLightPosition: { x: 10, y: 10 }
})
程序化地显示/隐藏交通灯
也可以不依赖构造选项,改在运行时从主进程通过 win.setWindowButtonVisibility(boolean) 强制切换交通灯可见性:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow()
// hides the traffic lights
win.setWindowButtonVisibility(false)
教程特别给出了一条等价性提示:由于相关 API 众多,达成同一布局有多种组合。例如 frame: false(无边框窗口)配合 win.setWindowButtonVisibility(true),最终布局效果与直接设置 titleBarStyle: 'hidden' 完全一致。你完全可以根据代码可读性选择任何一种组合。
进阶定制(跨平台):通过 Window Controls Overlay 定制窗口控制
Window Controls Overlay API 是一个 Web 标准,目标是让安装到桌面的 Web 应用能够自定义标题栏区域。Electron 通过 BrowserWindow 构造选项中的 titleBarOverlay 将其暴露给开发者:启用后,窗口控制按钮会在默认位置显露出来,且按钮下方的 DOM 区域不允许被页面内容覆盖。
当你需要进一步自定义按钮外观时,可以把 titleBarOverlay 从布尔值升级为对象。教程给出了一个完整的 macOS/Windows 通用示例(base-window-options.md 中称之为 Object 形态),其字段语义如下:
| 字段 | 适用平台 | 说明 | 默认值 |
|---|---|---|---|
color |
Windows、Linux | Window Controls Overlay 按钮所在条带的 CSS 颜色 | 系统颜色 |
symbolColor |
Windows、Linux | 按钮图标(最小化/最大化/关闭符号)的颜色 | 系统颜色 |
height |
全平台 | 标题栏与按钮区域高度(像素,必须是整数) | 系统标准高度 |
color 与 symbolColor 支持 rgba()、hsla() 以及带透明通道的 #RRGGBBAA 格式,即颜色本身可以带透明度;两个颜色属性若未指定,则回退到对应平台系统为窗口控制按钮使用的默认色:
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
titleBarStyle: 'hidden',
titleBarOverlay: {
color: '#2f3241',
symbolColor: '#74b1be',
height: 60
}
})
按此配置,在 Windows/Linux 上你会得到一个深色(#2f3241)、青蓝图标(#74b1be)、高 60px 的按钮条带;在 macOS 上仅 height 生效,交通灯仍保持系统外观。
主进程启用 overlay 之后,渲染进程侧即可通过两组能力感知 overlay 区域:
- 只读的 JavaScript API,用于查询 overlay 当前的尺寸与颜色值;
- CSS 环境变量(即前文用到的
env(titlebar-area-x / titlebar-area-width / titlebar-area-height))。
因此,真正"现代化"的自定义标题栏最佳实践通常是:构造窗口时用 titleBarStyle: 'hidden' + titleBarOverlay: { color, symbolColor, height },渲染层用 app-region: drag 撑起拖拽区、用 env(titlebar-area-*) 动态收缩内容避开按钮,再配合 titleBarOverlay 的只读 API 响应布局变化(如用户切到 RTL 或修改系统设置导致按钮换边)。这与本文第五步的 safe-area Fiddle 形成了完整的闭环。
小结
从本仓库的 custom-title-bar.md 教程出发,结合 base-window-options.md 的选项权威说明与 docs/fiddles/features/window-customization/custom-title-bar/ 下的逐步骤示例,我们可以把"自定义标题栏"的实现归纳为一条可复用的跨平台路径:
- 移除系统 chrome:
titleBarStyle: 'hidden',macOS 保留交通灯,Windows/Linux 无按钮; - 恢复窗口控制:Windows/Linux 追加
titleBarOverlay: true(可用对象形式自定义颜色与高度,但titleBarStyle不能为default); - 自绘标题栏:在 HTML 顶部放置标题栏容器;
- 恢复拖拽:容器加
app-region: drag,可交互子元素加app-region: no-drag; - 规避遮挡:用
env(titlebar-area-x/width/height)与合理回退值做安全区适配; - macOS 精调(可选):
customButtonsOnHover、hiddenInset、trafficLightPosition、setWindowButtonVisibility按需组合。
需要继续深入时可参考仓库中的 browser-window.md(BrowserWindow 实例方法全集)、base-window-options.md(全部构造选项的平台语义)以及 custom-window-interactions.md(拖拽区、窗口置顶等更多窗口交互定制)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00