How to Implement System Tray Functionality with Electron’s Tray and Menu APIs

To implement system tray functionality in Electron, instantiate the Tray class after the app emits the ready event, attach a Menu via tray.setContextMenu(), and maintain a global reference to prevent garbage collection from removing the icon.

Electron provides a native-level abstraction for system tray icons through the Tray class, enabling cross-platform notification-area integration for Windows, macOS, and Linux. According to the electron/electron source code, this API bridges JavaScript orchestration with platform-specific C++ implementations located in shell/browser/ui/. The following guide demonstrates how to implement system tray functionality with Tray and Menu using the actual architecture and file paths from the repository.

Creating the Tray Instance After App Ready

The Tray constructor must be called only after Chromium’s event loop initializes on the UI thread. In lib/browser/api/tray.ts, the JavaScript façade validates that the application is ready before creating the underlying native object.

Instantiate the tray by passing a NativeImage instance created via nativeImage.createFromPath(). The image is automatically converted to platform-specific formats—ICO for Windows, PNG or ICNS for macOS, and PNG for Linux.

const { app, Tray, nativeImage } = require('electron/main')

let tray = null

app.whenReady().then(() => {
  const icon = nativeImage.createFromPath('resources/tray-icon.png')
  tray = new Tray(icon)
})

Attaching a Context Menu

Context menus are built using Menu.buildFromTemplate() and attached with tray.setContextMenu(). Unlike standard window menus, tray menus do not require a manual menu.popup() call; the native implementation handles display automatically on right-click (Windows/Linux) or left-click (macOS).

const { Menu } = require('electron/main')

const contextMenu = Menu.buildFromTemplate([
  { label: 'Show Application', click: () => { mainWindow.show() } },
  { type: 'separator' },
  { role: 'quit' }
])

tray.setContextMenu(contextMenu)

The Menu object acts as a thin wrapper around native menu implementations, delegating platform rendering to the underlying operating system APIs.

Handling Tray Events

The Tray class emits events for user interactions, allowing custom behavior beyond the default context menu. Implement listeners for click, right-click, double-click, and balloon-click (Windows) to toggle window visibility or execute commands.

tray.on('click', () => {
  mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})

tray.on('right-click', () => {
  tray.popUpContextMenu()
})

Platform-Specific Native Implementation

The cross-platform capability relies on native C++ implementations that extend TrayIcon base classes:

  • Windows: shell/browser/ui/tray_icon_win.cc manages the notification area and balloon notifications via the Windows API.
  • macOS: shell/browser/ui/tray_icon_cocoa.mm interfaces with Cocoa’s NSStatusBar and NSStatusItem.
  • Linux: shell/browser/ui/tray_icon_linux.cc implements the system tray using libappindicator or GtkStatusIcon depending on the desktop environment.

These files handle OS-specific quirks such as icon scaling, dark mode detection, and menu positioning, while the JavaScript layer in lib/browser/api/tray.ts provides a unified API surface.

Preventing Garbage Collection

The Tray object is subject to JavaScript garbage collection like any other non-referenced variable. Losing the reference immediately removes the icon from the system tray. Store the instance in a module-level variable or attach it to a global object to ensure persistence throughout the application lifecycle.

// Global reference required
global.tray = new Tray(icon)
// OR
let tray = null // module-level scope

Summary

  • Initialize after ready: Create Tray instances only inside or after the app.whenReady() promise resolves to ensure the UI thread is active.
  • Attach menus via setContextMenu: Pass a Menu built from a template; the native layer handles popup positioning without manual intervention.
  • Maintain global scope: Store the tray reference at module level or higher to prevent garbage collection from destroying the native icon.
  • Handle platform differences: Expect right-click menus on Windows/Linux and left-click menus on macOS unless customized via event listeners.
  • Native source locations: Platform implementations reside in shell/browser/ui/tray_icon_win.cc, tray_icon_cocoa.mm, and tray_icon_linux.cc.

Frequently Asked Questions

Why does my tray icon disappear immediately after creation?

The Tray instance has been garbage collected. Because the JavaScript runtime does not detect native resources as retention roots, you must store the tray object in a global or module-level variable. As implemented in electron/electron, the native wrapper is destroyed when the V8 handle is finalized.

Can I use different icons for different platforms?

Yes. Provide platform-specific paths when calling nativeImage.createFromPath(). The native implementations in tray_icon_win.cc, tray_icon_cocoa.mm, and tray_icon_linux.cc automatically handle format requirements—ICO for Windows, ICNS or PNG for macOS, and PNG for Linux—though you may supply separate assets for optimal resolution and appearance.

How do I trigger the context menu on left-click instead of right-click?

On macOS, the context menu attached via setContextMenu() displays on left-click by default. On Windows and Linux, it displays on right-click. To standardize behavior or customize triggers, omit setContextMenu() and manually call tray.popUpContextMenu() inside a 'click' event listener defined in your main process.

Where is the Tray API implemented in the Electron source code?

The JavaScript API façade is located in lib/browser/api/tray.ts, which exports the binding created by electron_browser_tray. The platform-specific native logic resides in shell/browser/ui/tray_icon_win.cc (Windows), tray_icon_cocoa.mm (macOS), and tray_icon_linux.cc (Linux). Documentation references are available in docs/api/tray.md and docs/tutorial/tray.md.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →