Creating a menu in Electron.js is one of those tasks that seems simple, as is activating Debug mode… until you start adding submenus, roles, keyboard shortcuts, IPC channels, and, to top it off, cross-platform compatibility. In my case, the way I work with menus has evolved a lot: from simple templates to complete menus for real projects, such as an editor with functions for opening/saving, bold, italics, and H1/H2 titles sent to the renderer.
In this guide, I'm going to tell you how to create a menu in Electron.js step-by-step, with examples ready to copy/paste and, above all, with real-world cases that I use in my projects and courses.
What is an Electron.js Menu and How the Template Works
An Electron menu is simply an array of objects with a predefined structure. It's that simple. Each object represents a menu item and can include properties such as label, submenu, click, role, type, or accelerator.
That is, when I talk about a “template,” I am literally talking about something like:
const { Menu } = require('electron')
const template = [
{
label: 'About us',
submenu: [
{ label: 'About the app', click() { /* ... */ } }
]
}
];As you can see, the structure of the menus is just an array of objects with a predefined structure; we can define the options in the menu as two types: custom and with predefined roles, as we will present in the following sections.
Each menu is accompanied by a submenu in which we indicate the menu items, which can be custom or have predefined roles that we will see later; ultimately, and as you can see in the structure above, a menu is composed of an array, therefore, we can place as many items in our menu as we need; in turn, the submenu property is an array, so you can indicate as many items as you want.
Basic Structure: Array of Objects and Submenus
The pattern is always:
- Level 1 → Main Group (File, Edit, View, etc.)
- Level 2 → Submenus (Save, Open, Copy…)
- Level 3 → Actions or roles
This model is identical across all platforms, although macOS has some peculiarities (especially in the main menu).
Differences between Custom Options and Roles
Let's look at the two types of options we have
Custom Options
The main idea of menus is that we can include custom options to use throughout the application; therefore, they are used like a JavaScript click event associated with a button (for example), but, in this case, instead of a button, it is an option in a menu:
const { Menu, shell } = require('electron')
const template = [
{
role: 'About Electron',
submenu: [
{
label: "About the app",
click() {
shell.openExternal("https://www.electronjs.org")
}
},
]
},
]With the shell, we can open a web page in a link, as if it were a navigation link.
Their use is very simple, and they are the ones we use when we want to use some custom method.
They are actions that you define. For example, opening a URL, sending an event to the renderer, or activating a dark mode.
Predefined Roles
With roles, we already have specific functions that we can easily reuse:
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ role: 'togglefullscreen' }Which allow:
- reload, Reload the page.
- forceReload, Force the page to reload (they are like a hot reload).
- toggleDevTools, Activate the browser's developer console.
- resetZoom, Reset the zoom.
- zoomIn, Zoom In, increase the zoom.
- zoomOut, Zoom Out, decrease the zoom.
- togglefullscreen, Enable full-screen mode for the application.
We also have the separator for organizing menus:
{ type:'separator' },These are native functions that Electron already provides: reload, zoomIn, toggleDevTools, etc.
In a serious menu, I use both. For example, roles for system-related things, and custom actions to open files or apply styles.
Returning to the classification or options, we have two:
Creating a Custom Menu in Electron (Step-by-Step)
We create a simple menu like:
menu.js
const { app, ipcMain, Menu, shell, BrowserWindow, globalShortcut } = require('electron')
const { open_file, save_file } = require("./editor-options")
const template = [
{
label: 'About us',
click() {
shell.openExternal("https://www.electronjs.org")
}
},
{
label: 'File',
submenu: [
{
label: "Save",
accelerator: 'CommandOrControl+Shift+S',
click() {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editorchannel', 'file-save')
}
},
{
label: "Open",
accelerator: 'CommandOrControl+Shift+O',
click() {
const win = BrowserWindow.getFocusedWindow()
open_file(win)
}
},
]
},
{
label: 'Style and format',
submenu: [
{
label: 'Bold',
click() {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editorchannel', 'style-bold')
console.log("Negritas")
}
},
{
label: 'Italic',
click() {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editor-channel', 'style-italic')
}
},
{
type: 'separator'
},
{
label: 'H1',
click() {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editor-channel', 'style-h1')
}
},
{
label: 'H2',
click() {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editor-channel', 'style-h2')
}
},
]
}
]
if (process.platform == 'win32' || process.platform == 'darwin') {
template.push(
{
label: 'Default',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{
type: 'separator'
},
{ role: 'togglefullscreen' },
]
}
)
}
ipcMain.on('editor-channel', (event, arg) => {
console.log("Mensaje recibido del canal 'editor-channel': " + arg)
})
ipcMain.on('file-open', (event, arg) => {
const win = BrowserWindow.getFocusedWindow()
open_file(win)
})
ipcMain.on('file-save', (event, arg) => {
const win = BrowserWindow.getFocusedWindow()
save_file(win, arg)
})
app.on('ready', () => {
globalShortcut.register('CommandOrControl+Shift+S', () => {
const win = BrowserWindow.getFocusedWindow()
win.webContents.send('editor-channel', 'file-save')
})
globalShortcut.register('CommandOrControl+Shift+O', () => {
const win = BrowserWindow.getFocusedWindow()
open_file(win)
})
});
const menu = Menu.buildFromTemplate(template);
module.exports = menuTo consume it, we have, from the main file:
const menu = require('./menu')
***
app.whenReady().then(createWindow)
Menu.setApplicationMenu(menu)And by executing an:
npm run startWe get:
FAQs about Electron.js Menus
- Where to put menu.js?
- In the project root, next to the main file.
- How to create different menus per platform?
- Using:
- if (process.platform === 'darwin') { ... }
- How to combine several submenus without breaking the template?
- Always check that each submenu is a valid array.
Conclusion
Creating a menu in Electron.js can be as simple or as advanced as you need it to be. The nice thing is that you have a system that allows you to mix native roles, custom actions, IPC communication, global shortcuts, and integration with active windows. In my case, I use these patterns for real applications—from editors to utilities—and I always follow the same flow: template → external file → roles + actions → IPC → shortcuts → cross-platform.
The next step is to learn how to use keyboard shortcuts in Electron.
I agree to receive announcements of interest about this Blog.
We're going to learn how to manage cross-platform menus in Electron.js, learn about the types of actions defined by us or by systems, and recommendations.