Creating an ElectronJS Menubar App with a Tray Icon

Jason Gilmore

This tutorial will show you how to add a custom icon to your Electron-powered menubar app.

Set Up the Main Process

First, create a Tray instance in your main process:

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

let tray = null

app.whenReady().then(() => {
  createTray()
})

function createTray() {
  // Create the tray with an empty icon initially
  tray = new Tray(nativeImage.createEmpty())

  // Set basic properties
  tray.setTitle('My App')
  tray.setToolTip('My App')
}

Create the Icon

Your tray icon should be:

  • PNG format
  • Small size (recommended 16x16 or 32x32 pixels)
  • Preferably with transparency
  • Visible against both light and dark menubar backgrounds
function createTray() {
  // Create the tray with an empty icon initially
  tray = new Tray(nativeImage.createEmpty())

  // Load and set the icon
  const iconPath = path.join(__dirname, 'assets/icon.png')
  if (fs.existsSync(iconPath)) {
    const icon = nativeImage.createFromPath(iconPath)
    const resizedIcon = icon.resize({ width: 24, height: 24 })
    tray.setImage(resizedIcon)
  }
}

Handle Dark/Light Mode

To support both dark and light modes, use the nativeTheme API:

const { nativeTheme } = require('electron')

function createTray() {
  // ... previous tray setup code ...

  // Listen for theme changes
  nativeTheme.on('updated', () => {
    const iconPath = nativeTheme.shouldUseDarkColors
      ? path.join(__dirname, 'assets/icon-dark.png')
      : path.join(__dirname, 'assets/icon-light.png')

    if (fs.existsSync(iconPath)) {
      const icon = nativeImage.createFromPath(iconPath)
      const resizedIcon = icon.resize({ width: 24, height: 24 })
      tray.setImage(resizedIcon)
    }
  })
}

Hide Dock Icon

Since this is a menubar app, you probably want to hide the dock icon otherwise it will consistently take up valuable space:

if (process.platform === 'darwin') {
  app.dock.hide()
}

Add Click Handlers

Make your tray icon interactive by adding click handlers:

function createTray() {
  // ... previous tray setup code ...

  tray.on('click', (event) => {
    // Handle left click
  })

  tray.on('right-click', (event) => {
    // Handle right click, perhaps show a context menu
  })
}

Complete Example

Here's a complete example with all of the above concepts incorporated:

const { app, Tray, nativeImage, nativeTheme } = require('electron')
const path = require('path')
const fs = require('fs')

let tray = null

app.whenReady().then(() => {
  // Hide dock icon for menubar apps
  if (process.platform === 'darwin') {
    app.dock.hide()
  }

  createTray()
})

function createTray() {
  // Create with empty icon initially
  tray = new Tray(nativeImage.createEmpty())

  // Set basic properties
  tray.setTitle('')
  tray.setToolTip('My App')

  // Load initial icon
  updateTrayIcon()

  // Listen for theme changes
  nativeTheme.on('updated', () => {
    updateTrayIcon()
  })

  // Handle clicks
  tray.on('click', (event) => {
    // Handle left click
  })
}

function updateTrayIcon() {
  const iconPath = nativeTheme.shouldUseDarkColors
    ? path.join(__dirname, 'assets/icon-dark.png')
    : path.join(__dirname, 'assets/icon-light.png')

  if (fs.existsSync(iconPath)) {
    const icon = nativeImage.createFromPath(iconPath)
    const resizedIcon = icon.resize({ width: 24, height: 24 })
    tray.setImage(resizedIcon)
  }
}
electrontypescriptmenubarmacos