Creating an Electron tray app and correctly positioning it on Windows, Linux, and Mac
Patrick Passarella - 7th Nov, 2020
Photo by Aziz Acharki on Unsplash
I've had a hard time trying to have the tray window appear in the correct spot for all platforms, since in Linux the tray.getBounds()
doesn't work.
To solve that, we can use the mouse position for Linux, and the tray.getBounds()
for windows and mac.
First, let's create an Electron app (I will not enter in details here, just a quick step-by-step list). You can also jump directly to the important part, here.
$ yarn init
to create an empty project.main
property in the package.json
to main.js
.main.js
file in the root of your project.$ yarn add electron --dev
."start": "electron ."
index.html
in your project root.In the main.js
file, I will be using the Electron default code, found on their documentation. With just some adjustments.
1const { app, BrowserWindow, Tray } = require('electron');2const path = require('path');34// Keep a global reference of the window object, if you don't, the window will5// be closed automatically when the JavaScript object is garbage collected.6let tray = null;7let window = null;89function createWindow() {10 // Create the browser window.11 window = new BrowserWindow({12 width: 800,13 height: 600,14 webPreferences: {15 nodeIntegration: true,16 },17 });1819 // and load the index.html of the app.20 window.loadFile('index.html');2122 // Open the DevTools.23 window.webContents.openDevTools({ mode: 'detach' });24}2526app.on('ready', () => {27 createWindow();28});2930app.on('window-all-closed', () => {31 if (process.platform !== 'darwin') {32 app.quit();33 }34});3536app.on('activate', () => {37 // On macOS it's common to re-create a window in the app when the38 // dock icon is clicked and there are no other windows open.39 if (BrowserWindow.getAllWindows().length === 0) {40 createWindow();41 }42});
In the index.html
, I will also be using the same code as the Electron documentation (just a "Hello World").
1<!DOCTYPE html>2<html>3 <head>4 <meta charset="UTF-8" />5 <title>Hello World!</title>6 <meta7 http-equiv="Content-Security-Policy"8 content="script-src 'self' 'unsafe-inline';"9 />10 </head>11 <body>12 <h1>Hello World!</h1>13 </body>14</html>
Now, let's create a tray icon. In the main.js
file, create a function createTray
. Remember to import Tray
from 'electron'
.
This function will create the tray with the icon image provided in the assets folder, so you need to create that folder and put the image inside.
1const createTray = () => {2 tray = new Tray(path.join(__dirname, 'assets', 'icon.png'));3};
Call this function only after the app is ready.
1app.on('ready', () => {2 createWindow();3 createTray();4});
Run the app again with $ yarn start
, you will see that the tray will appear with your icon.
Now for the main part, clicking the tray icon to open the window in the correct position.
Change the show
option to false in the BrowserWindow
, to not show the window after created.
1const window = new BrowserWindow({2 width: 800,3 height: 600,4 show: false, // <- here5 webPreferences: {6 nodeIntegration: true,7 },8});
Create a function to toggle the window. So whenever we click the tray icon, it will show the window if it's hidden, and hide if it's not.
1const toggleWindow = () => {2 if (window.isVisible()) return window.hide();3 return showWindow();4};
That showWindow
function, which is being used inside the toggleWindow
, is the function that will correctly position the window before showing it. For that, we will be using the library electron-traywindow-positioner, which has everything built in to position the tray icon for all platforms, using the mouse position for Linux, and the tray bounds for windows and mac.
Install the library $ yarn add electron-traywindow-positioner
and import it. After calling the positioner.position
, it will change the position of the window, so you can just show it using window.show()
.
1const positioner = require('electron-traywindow-positioner');23const showWindow = () => {4 positioner.position(window, tray.getBounds());5 window.show();6};
Then, add a click handler to the tray, which will call that function.
1const createTray = () => {2 tray = new Tray(path.join(__dirname, 'assets', 'icon.png'));3 tray.on('click', () => {4 toggleWindow();5 });6};
Now, every time you click the tray icon, it will toggle the window, and with the correct position!
Here is the full code in the main.js
file.
1const { app, BrowserWindow, Tray } = require('electron');2const positioner = require('electron-traywindow-positioner');3const path = require('path');45let window = null;6let tray = null;78const showWindow = () => {9 positioner.position(window, tray.getBounds());10 window.show();11};1213const toggleWindow = () => {14 if (window.isVisible()) return window.hide();15 return showWindow();16};1718const createTray = () => {19 tray = new Tray(path.join(__dirname, 'assets', 'drop.png'));20 tray.on('click', () => {21 toggleWindow();22 });23};2425const createWindow = () => {26 // Create the browser window.27 window = new BrowserWindow({28 width: 800,29 height: 600,30 show: false,31 webPreferences: {32 nodeIntegration: true,33 },34 });3536 // and load the index.html of the app.37 window.loadFile('index.html');3839 // Open the DevTools.40 window.webContents.openDevTools({ mode: 'detach' });41};4243app.on('ready', () => {44 createWindow();45 createTray();46});4748app.on('window-all-closed', () => {49 if (process.platform !== 'darwin') {50 app.quit();51 }52});5354app.on('activate', () => {55 // On macOS it's common to re-create a window in the app when the56 // dock icon is clicked and there are no other windows open.57 if (BrowserWindow.getAllWindows().length === 0) {58 createWindow();59 }60});
That's it, very simple, but it's something that took me some time to find a great and simple solution.