Skip to content

Electron - Menus

Desktop applications typically feature two kinds of menus: the main application menu (usually at the top of the screen or window) and context menus (which appear on right-click). Electron provides modules to create both.

You’ll primarily use the Menu and MenuItem modules from Electron. These modules are available in the main process. To create dynamic context menus triggered from a renderer process, you’ll use Inter-Process Communication (IPC) to ask the main process to create and display the menu. This is a more secure approach than using the deprecated remote module.

Application menus are defined in the main process. Here’s an example of setting up a typical application menu in your main.js file:

const { app, BrowserWindow, Menu, ipcMain } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // Important for IPC
contextIsolation: true, // Recommended for security
nodeIntegration: false // Recommended for security
}
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// mainWindow.webContents.openDevTools(); // Optional: opens DevTools
}
// Application Menu Template
const menuTemplate = [
{
label: 'File',
submenu: [
{ label: 'New Window', click: () => { /* Implement New Window Logic */ } },
{ type: 'separator' },
app.isPackaged ? { role: 'close' } : { role: 'quit' } // Shows 'Close' or 'Quit' appropriately
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' }, // On macOS, this is the green button behavior
...(process.platform === 'darwin' ? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' } // The 'Window' menu on macOS
] : [
{ role: 'close' }
])
]
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron');
await shell.openExternal('https://electronjs.org');
}
}
]
}
];
// Adapt menu for macOS
if (process.platform === 'darwin') {
menuTemplate.unshift({
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
});
}
app.on('ready', () => {
createWindow();
const mainMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(mainMenu);
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC handler for context menu (see next section)
ipcMain.on('show-context-menu', (event) => {
const contextMenuTemplate = [
{
label: 'Inspect Element',
click: () => {
BrowserWindow.fromWebContents(event.sender)?.webContents.inspectElement(0, 0);
}
},
{ type: 'separator' },
{ label: 'Custom Action 1', click: () => console.log('Custom Action 1 executed') },
];
const menu = Menu.buildFromTemplate(contextMenuTemplate);
menu.popup({ window: BrowserWindow.fromWebContents(event.sender) });
});
// Ensure you have an index.html and preload.js in your project directory.

This code sets up a standard application menu. We define a menuTemplate (an array of objects) and then build and set it using Menu.buildFromTemplate() and Menu.setApplicationMenu(). The role property allows Electron to use predefined behaviors and labels for common menu items, adapting them to the operating system.

Notice the webPreferences in BrowserWindow. contextIsolation and nodeIntegration: false are key security practices. The preload.js script is essential for securely exposing specific Node.js/Electron functionalities to your renderer process via IPC.

Creating a Context Menu (Right-Click Menu) via IPC

Section titled “Creating a Context Menu (Right-Click Menu) via IPC”

Context menus are typically triggered from the renderer process (your HTML/JavaScript files). Since the remote module is deprecated for security reasons, we use IPC to request the main process to show a context menu.

Here’s how you can set it up:

This script runs in a privileged environment and can access both the window object of the renderer and Node.js modules. It acts as a bridge.

// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: () => ipcRenderer.send('show-context-menu')
// You can pass arguments here if your context menu needs data from the renderer
// e.g., showContextMenu: (someData) => ipcRenderer.send('show-context-menu', someData)
});

This code exposes a window.electronAPI.showContextMenu function to your renderer process, which, when called, sends an IPC message named ‘show-context-menu’ to the main process.

In your HTML file, add a script to listen for the contextmenu event and call the exposed API.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Electron Menus Demo</title>
<style>
body { font-family: sans-serif; height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; }
.container { padding: 20px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; }
</style>
</head>
<body>
<div class="container">
<p>Right-click anywhere in this window to see a context menu.</p>
</div>
<script>
// This script runs in the renderer process
window.addEventListener('contextmenu', (event) => {
event.preventDefault(); // Prevent the default browser context menu
window.electronAPI.showContextMenu(); // Call the API exposed by preload.js
}, false);
</script>
</body>
</html>

The main process needs to listen for the ‘show-context-menu’ message. We already added this handler in the main.js example in the Application Menu section. Here it is again for clarity:

// Add this to your main.js if not already present
// const { ipcMain, Menu, BrowserWindow } = require('electron'); // Ensure these are required
ipcMain.on('show-context-menu', (event) => {
const contextMenuTemplate = [
{
label: 'Inspect Element',
click: () => {
// event.sender is the WebContents that sent the message
BrowserWindow.fromWebContents(event.sender)?.webContents.inspectElement(0, 0);
}
},
{ type: 'separator' },
{ label: 'Custom Action 1', click: () => console.log('Custom Action 1 executed from context menu') },
{ label: 'Another Action', type: 'checkbox', checked: true }
];
const menu = Menu.buildFromTemplate(contextMenuTemplate);
// menu.popup() displays the menu.
// We pass the window associated with the renderer process that sent the event.
menu.popup({ window: BrowserWindow.fromWebContents(event.sender) });
});

When you right-click in your application window, the contextmenu event is caught in index.html, window.electronAPI.showContextMenu() is called, which sends an IPC message. The ipcMain listener in main.js receives this message, builds a menu from the contextMenuTemplate, and displays it at the cursor’s position.

This IPC-based approach is more secure and aligns with modern Electron development practices. It clearly separates concerns between the main and renderer processes.