Electron provides powerful menu and system tray features that allow applications to better integrate with the operating system. This article will detail how to implement menu and tray functionality in Electron.
Application Menu
1. Creating Application Menu
javascript// main.js const { app, Menu, BrowserWindow } = require('electron') let mainWindow app.whenReady().then(() => { createWindow() createMenu() }) function createMenu() { const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => { console.log('New file') } }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => { console.log('Open file') } }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => { console.log('Save file') } }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' } ] }, { label: 'View', submenu: [ { label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' }, { label: 'Toggle Developer Tools', accelerator: 'CmdOrCtrl+Shift+I', role: 'toggleDevTools' }, { type: 'separator' }, { label: 'Actual Size', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, { label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, { type: 'separator' }, { label: 'Toggle Full Screen', accelerator: 'F11', role: 'togglefullscreen' } ] }, { label: 'Window', submenu: [ { label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' }, { label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' } ] }, { label: 'Help', submenu: [ { label: 'About', click: () => { console.log('About') } }, { label: 'Documentation', click: () => { shell.openExternal('https://electronjs.org/docs') } } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) }
2. Dynamic Menu
javascript// main.js let recentFiles = [] function updateRecentFilesMenu() { const recentMenu = recentFiles.map(file => ({ label: file, click: () => { openFile(file) } })) const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Open Recent', submenu: recentMenu.length > 0 ? recentMenu : [{ label: 'No recent files', enabled: false }] }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } function addRecentFile(filePath) { recentFiles = recentFiles.filter(f => f !== filePath) recentFiles.unshift(filePath) recentFiles = recentFiles.slice(0, 10) // Keep at most 10 updateRecentFilesMenu() }
3. Context Menu
javascript// main.js const { contextMenu } = require('electron') app.whenReady().then(() => { // Enable context menu contextMenu({ prepend: (params, browserWindow) => [ { label: 'Custom Action', click: () => { console.log('Custom action clicked') } } ] }) }) // Or manually create context menu function createContextMenu() { const template = [ { label: 'Copy', accelerator: 'CmdOrCtrl+C', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.copy() } }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.paste() } }, { type: 'separator' }, { label: 'Inspect Element', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.inspectElement() } } ] const menu = Menu.buildFromTemplate(template) return menu } // Use in renderer process // renderer.js const { remote } = require('electron') document.addEventListener('contextmenu', (event) => { event.preventDefault() const menu = remote.getGlobal('contextMenu') menu.popup({ window: remote.getCurrentWindow() }) })
System Tray
1. Creating Tray Icon
javascript// main.js const { app, Tray, Menu, nativeImage } = require('electron') const path = require('path') let tray = null app.whenReady().then(() => { createTray() }) function createTray() { // Create tray icon const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // Set icon size tray = new Tray(icon.resize({ width: 16, height: 16 })) // Set tooltip tray.setToolTip('My Application') // Create tray menu const contextMenu = Menu.buildFromTemplate([ { label: 'Show App', click: () => { showWindow() } }, { label: 'Hide App', click: () => { hideWindow() } }, { type: 'separator' }, { label: 'Settings', click: () => { openSettings() } }, { type: 'separator' }, { label: 'Quit', click: () => { app.quit() } } ]) tray.setContextMenu(contextMenu) // Double-click tray icon tray.on('double-click', () => { showWindow() }) // Single-click tray icon tray.on('click', () => { toggleWindow() }) }
2. Tray Icon Animation
javascript// main.js let trayAnimationInterval = null function startTrayAnimation() { const icons = [ path.join(__dirname, 'icon1.png'), path.join(__dirname, 'icon2.png'), path.join(__dirname, 'icon3.png') ] let currentIndex = 0 trayAnimationInterval = setInterval(() => { const icon = nativeImage.createFromPath(icons[currentIndex]) tray.setImage(icon.resize({ width: 16, height: 16 })) currentIndex = (currentIndex + 1) % icons.length }, 500) } function stopTrayAnimation() { if (trayAnimationInterval) { clearInterval(trayAnimationInterval) trayAnimationInterval = null } // Restore default icon const defaultIcon = nativeImage.createFromPath(path.join(__dirname, 'icon.png')) tray.setImage(defaultIcon.resize({ width: 16, height: 16 })) }
3. Tray Notifications
javascript// main.js const { Notification } = require('electron') function showTrayNotification(title, body) { if (Notification.isSupported()) { const notification = new Notification({ title, body, icon: path.join(__dirname, 'icon.png'), silent: false }) notification.on('click', () => { showWindow() }) notification.show() } } // Usage example ipcMain.on('show-notification', (event, { title, body }) => { showTrayNotification(title, body) })
Window Control
1. Minimize to Tray
javascript// main.js let mainWindow function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.on('minimize', (event) => { event.preventDefault() mainWindow.hide() }) mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault() mainWindow.hide() } }) } function showWindow() { if (mainWindow) { mainWindow.show() mainWindow.focus() } } function hideWindow() { if (mainWindow) { mainWindow.hide() } } function toggleWindow() { if (mainWindow.isVisible()) { hideWindow() } else { showWindow() } } app.on('before-quit', () => { app.isQuitting = true })
2. Dynamic Tray Menu Updates
javascript// main.js function updateTrayMenu() { const isPlaying = getPlaybackState() const template = [ { label: isPlaying ? 'Pause' : 'Play', click: () => { togglePlayback() updateTrayMenu() } }, { label: 'Next Track', click: () => { nextTrack() } }, { label: 'Previous Track', click: () => { previousTrack() } }, { type: 'separator' }, { label: 'Show App', click: () => { showWindow() } }, { label: 'Quit', click: () => { app.quit() } } ] const contextMenu = Menu.buildFromTemplate(template) tray.setContextMenu(contextMenu) }
Cross-Platform Compatibility
1. Platform-Specific Menus
javascript// main.js function createMenu() { const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => saveFile() } ] } ] // macOS specific menu if (process.platform === 'darwin') { template.unshift({ label: app.getName(), submenu: [ { label: 'About ' + app.getName(), role: 'about' }, { type: 'separator' }, { label: 'Preferences', accelerator: 'Cmd+,', click: () => openPreferences() }, { type: 'separator' }, { label: 'Services', role: 'services', submenu: [] }, { type: 'separator' }, { label: 'Hide ' + app.getName(), accelerator: 'Command+H', role: 'hide' }, { label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' }, { label: 'Show All', role: 'unhide' }, { type: 'separator' }, { label: 'Quit', accelerator: 'Command+Q', click: () => app.quit() } ] }) } const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) }
2. Platform-Specific Accelerators
javascript// main.js function getAccelerator(action) { const accelerators = { 'new': 'CmdOrCtrl+N', 'open': 'CmdOrCtrl+O', 'save': 'CmdOrCtrl+S', 'quit': process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', 'preferences': process.platform === 'darwin' ? 'Cmd+,' : 'Ctrl+,' } return accelerators[action] || '' }
3. Tray Icon Adaptation
javascript// main.js function createTray() { let iconPath if (process.platform === 'darwin') { // macOS uses template icon iconPath = path.join(__dirname, 'iconTemplate.png') } else if (process.platform === 'win32') { // Windows uses ICO format iconPath = path.join(__dirname, 'icon.ico') } else { // Linux uses PNG format iconPath = path.join(__dirname, 'icon.png') } const icon = nativeImage.createFromPath(iconPath) // macOS needs to set template property if (process.platform === 'darwin') { icon.setTemplateImage(true) } tray = new Tray(icon) tray.setToolTip('My Application') }
Best Practices
1. Menu Organization
javascript// main.js // Follow platform conventions function createMenu() { const template = [ // macOS: App name menu // Windows/Linux: File menu { label: process.platform === 'darwin' ? app.getName() : 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => saveFile() }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, // Edit menu { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) }
2. Tray Icon Design
javascript// main.js // Use high contrast icons function createTray() { const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // Ensure icon is visible on different backgrounds const iconSize = process.platform === 'win32' ? 16 : 22 tray = new Tray(icon.resize({ width: iconSize, height: iconSize })) tray.setToolTip('My Application') }
3. User Experience
javascript// main.js // Provide clear menu items function createTrayMenu() { const template = [ { label: 'Show Window', click: () => showWindow() }, { type: 'separator' }, { label: 'Settings', click: () => openSettings() }, { label: 'Help', click: () => openHelp() }, { type: 'separator' }, { label: 'Exit', click: () => app.quit() } ] const menu = Menu.buildFromTemplate(template) tray.setContextMenu(menu) }
Common Questions
Q: How to create template icon on macOS?A: Use setTemplateImage(true) method:
javascriptconst icon = nativeImage.createFromPath(iconPath) icon.setTemplateImage(true) tray = new Tray(icon)
Q: How to disable default menu?A: Set empty menu:
javascriptMenu.setApplicationMenu(null)
Q: How to add checkbox in tray menu?A: Use type: 'checkbox':
javascript{ label: 'Enable Feature', type: 'checkbox', checked: true, click: (menuItem) => { console.log('Checked:', menuItem.checked) } }
Q: How to create system tray balloon notification on Windows?A: Use display-balloon:
javascripttray.displayBalloon({ title: 'Notification', content: 'This is a balloon notification' })