This tutorial gives details about how to build a hamburger type menu bar in electron apps similar to slack's menu bar. This type of menu bar declutters the title bar when your application's menu items are rarely used.
Pre-requisite
Basics of ElectronJS. Check this tutorial to get started.
Resources
Finished code is available at https://github.com/saisandeepvaddi/electron-custom-menu-bar
What we'll build
Here is what it is going to look when we finish.
Set up electron project
Set up a minimal electron app from electron's official quick start github repo.
# Clone the Quick Start repository
$ git clone https://github.com/electron/electron-quick-start
# Go into the repository
$ cd electron-quick-start
# Install the dependencies and run
$ npm install && npm start
Main process code
When you first run npm start
you will see a window with a default menu bar
attached to it. To replace it with our menu bar, we need to do two things. In
the main.js
file we have,
- Set the
frame: false
in theoptions
object fornew BrowserWindow({frame: false, ...other-options})
. This will create a window without toolbars, borders, etc., Check frameless-window for more details. - Register an event listener on
ipcMain
which receives a mouse click position when the mouse is clicked on the hamburger icon.
// main.js
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js")
},
frame: false // Remove frame to hide default menu
});
// ...other stuff
}
// Register an event listener.
// ipcRenderer sends mouse click coordinates.
// Shows menu pop-up at those coordinates.
ipcMain.on(`display-app-menu`, function(e, args) {
if (isWindows && mainWindow) {
menu.popup({
window: mainWindow,
x: args.x,
y: args.y
});
}
});
// ... other stuff.
Create a file called menu-functions.js
and define these functions. All the
functions here take electron's BrowserWindow
object (mainWindow
in this app)
and run minimize, maximize, close, open menu actions which we need to trigger
from our custom menu bar.
// menu-functions.js
const { remote, ipcRenderer } = require("electron");
function getCurrentWindow() {
return remote.getCurrentWindow();
}
function openMenu(x, y) {
ipcRenderer.send(`display-app-menu`, { x, y });
}
function minimizeWindow(browserWindow = getCurrentWindow()) {
if (browserWindow.minimizable) {
// browserWindow.isMinimizable() for old electron versions
browserWindow.minimize();
}
}
function maximizeWindow(browserWindow = getCurrentWindow()) {
if (browserWindow.maximizable) {
// browserWindow.isMaximizable() for old electron versions
browserWindow.maximize();
}
}
function unmaximizeWindow(browserWindow = getCurrentWindow()) {
browserWindow.unmaximize();
}
function maxUnmaxWindow(browserWindow = getCurrentWindow()) {
if (browserWindow.isMaximized()) {
browserWindow.unmaximize();
} else {
browserWindow.maximize();
}
}
function closeWindow(browserWindow = getCurrentWindow()) {
browserWindow.close();
}
function isWindowMaximized(browserWindow = getCurrentWindow()) {
return browserWindow.isMaximized();
}
module.exports = {
getCurrentWindow,
openMenu,
minimizeWindow,
maximizeWindow,
unmaximizeWindow,
maxUnmaxWindow,
isWindowMaximized,
closeWindow,
};
We need to attach these functions to the window
object which we can use in the
renderer process. If you are using older versions (<5.0.0) of electron or you
set nodeIntegration: true
in BrowserWindow
's options, you can use the above
menu-functions.js
file directly in the renderer process. Electron new versions
have it false
set by default for
security reasons.
// preload.js
const { remote } = require("electron");
const {
getCurrentWindow,
openMenu,
minimizeWindow,
unmaximizeWindow,
maxUnmaxWindow,
isWindowMaximized,
closeWindow,
} = require("./menu-functions");
window.addEventListener("DOMContentLoaded", () => {
window.getCurrentWindow = getCurrentWindow;
window.openMenu = openMenu;
window.minimizeWindow = minimizeWindow;
window.unmaximizeWindow = unmaximizeWindow;
window.maxUnmaxWindow = maxUnmaxWindow;
window.isWindowMaximized = isWindowMaximized;
window.closeWindow = closeWindow;
});
We need a menu now. Create a simple menu in a new menu.js
file. You can learn
how to add your own options to the menu at
official docs. Electron has some
easy to follow documentation with examples.
// menu.js
const { app, Menu } = require("electron");
const isMac = process.platform === "darwin";
const template = [
{
label: "File",
submenu: [isMac ? { role: "close" } : { role: "quit" }],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
module.exports = {
menu,
};
We are done on the main process side. Now, let's build our custom menu bar. If you see the menu in the image, you'll see that we have these things on our menu bar.
- On the left side, a hamburger icon which is where the menu will open.
- On the right side, we have minimize button, maximize-unmaximize button, and close button.
I used fontawesome js file from fontawesome.com for
icons. Add it to HTML's <head>
tag. I removed Content-Security-Policy
meta
tags to allow fontawesome js file to run for now. In production, make sure you
properly allow which code should run. Check
CSP for more details.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<title>My Awesome App</title>
<link rel="stylesheet" href="style.css" />
<script
src="https://kit.fontawesome.com/1c9144b004.js"
crossorigin="anonymous">
</script>
</head>
</head>
<body>
<div id="menu-bar">
<div class="left" role="menu">
<button class="menubar-btn" id="menu-btn">
<i class="fas fa-bars"></i>
</button>
<h5>My Awesome App</h5>
</div>
<div class="right">
<button class="menubar-btn" id="minimize-btn">
<i class="fas fa-window-minimize"></i>
</button>
<button class="menubar-btn" id="max-unmax-btn">
<i class="far fa-square"></i>
</button>
<button class="menubar-btn" id="close-btn">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="container">
Hello there!
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
/* style.css */
body {
padding: 0;
margin: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: white;
}
#menu-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
background: #34475a;
-webkit-app-region: drag;
}
#menu-bar > div {
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.menubar-btn {
-webkit-app-region: no-drag;
}
.container {
height: calc(100vh - 30px);
background: #34475ab0;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
}
button {
height: 100%;
padding: 0 15px;
border: none;
background: transparent;
outline: none;
}
button:hover {
background: rgba(221, 221, 221, 0.2);
}
#close-btn:hover {
background: rgb(255, 0, 0);
}
button i {
color: white;
}
Now your window should look like this. Awesome. We are almost there.
If you guessed it, none of the buttons in the menu bar work. Because we didn't
add onclick
event listeners for them. Let's do that. Remember we attached some
utility functions to the window
object in preload.js
? We'll use them in
button click listeners.
// renderer.js
window.addEventListener("DOMContentLoaded", () => {
const menuButton = document.getElementById("menu-btn");
const minimizeButton = document.getElementById("minimize-btn");
const maxUnmaxButton = document.getElementById("max-unmax-btn");
const closeButton = document.getElementById("close-btn");
menuButton.addEventListener("click", (e) => {
// Opens menu at (x,y) coordinates of mouse click -
// - on the hamburger icon.
window.openMenu(e.x, e.y);
});
minimizeButton.addEventListener("click", (e) => {
window.minimizeWindow();
});
maxUnmaxButton.addEventListener("click", (e) => {
const icon = maxUnmaxButton.querySelector("i.far");
window.maxUnmaxWindow();
// Change the middle maximize-unmaximize icons.
if (window.isWindowMaximized()) {
icon.classList.remove("fa-square");
icon.classList.add("fa-clone");
} else {
icon.classList.add("fa-square");
icon.classList.remove("fa-clone");
}
});
closeButton.addEventListener("click", (e) => {
window.closeWindow();
});
});
That's it. Restart your app with npm run start
and your new menu bar buttons
should work.
NOTE: Some parts of code are removed in the above scripts for brevity. You can get the full code at https://github.com/saisandeepvaddi/electron-custom-menu-bar.
If you want to see a bigger electron app with a lot more stuff, check the https://github.com/saisandeepvaddi/ten-hands app which uses the similar style menu bar (custom style menu bar is visible only on Windows for now though) but built with React and TypeScript. I wrote this tutorial after using this menu bar there.
Thank you. 🙏