In the previous chapter, we created an application that lived in the menu bar on macOS or the system tray on Windows. Out of the box, Electron’s tray module allows you to set a menu to display when the user clicks the tray icon. This is the same type of menu used in the application and context menus that we built in Fire Sale. This menu also has the same limitations: it is limited to text, is hard to modify, and provides limited functionality.
Being able to build applications that live in the system tray or menu bar allows us to build entire classes of applications that we couldn’t build in the browser. It’s unfortunate that we have these restrictions, but—luckily—we can work around them. In this chapter, we explore a clever way to get around the limitations of the tray module using a third-party library conveniently called menubar.
menubar is an abstraction built on a set of core Electron modules that we’ve used previously in the book. A high-level explanation is that it creates an empty tray module. When the user clicks the icon, menubar shows a frameless, correctly positioned BrowserWindow instance beneath the icon, which creates the illusion that it’s attached to the icon. menubar also provides a cute cat icon by default.
In this chapter, we’ll rebuild Clipmaster from the ground up into a completely new application. This time it has a much more pleasant UI, shown in figure 10.1, compared to its predecessor. We add the ability to remove a clipping from the list or publish it to the web using an example API so that it can be shared publicly. Finally, we add some interactivity to our notifications and global shortcuts to trigger the application’s functionality using predetermined keystrokes.

The menubar library provides a function that allows you to create new menu bar applications. It uses the app module to control the lifecycle of the application, an instance from the tray module to create the icon in the operating system’s menu bar or system tray, and a BrowserWindow instance for displaying the UI (as shown in figure 10.2). menubar uses another third-party library called electron-positioner to correctly position the BrowserWindow instance under the icon. It also provides methods for hiding and showing the window programmatically.

A boilerplate for Clipmaster 9000 is available on Github (https://github.com/electron-in-action/clipmaster-9000). You can start on the master branch and code along or check out the completed-example branch to see the code in its state at the end of the chapter.
const Menubar = require('menubar');
const menubar = Menubar(); 1
menubar.on('ready', () => { 2
console.log('Application is ready.');
});
In listing 10.1, we don’t require the app module from Electron. menubar does that for us when we call the function and create the instance. You can also see that we’re listening for the ready event on menubar instead of app. menubar’s event is waiting on getting everything else set up, in addition to listening to the app’s ready event.
You can start the simple application using the npm start command. If all goes well, you should see a message in your terminal as well as a small cat in either your menu bar or system tray, depending on which platform the application is running on. If you click the icon, you see an empty browser window. menubar created a BrowserWindow instance on our behalf, but it did not load an HTML document into the window.
To get a UI in that window, we need to do a few things shown in the listing 10.2. First, we need to create an HTML document with some basic markup. Second, we need to define a CSS to style the UI. Third, we need the HTML document to load the code for our UI from renderer.js.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline';
connect-src https://cliphub.glitch.com/*
"
>
<!--
Change the URL in the line above if you fork the back end server.
-->
<title>Clipmaster 9000</title>
<link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
<div class="container">
<section class="controls"> 1
<button id="copy-from-clipboard">Copy from Clipboard</button>
</section>
<section class="content">
<div id="clippings-list"></div> 2
</section>
</div>
<script>
require('./renderer'); 3
</script>
</body>
</html>
I included the stylesheet in the repository, but let’s highlight some of the interesting bits next. I use one or two Electron-specific techniques in the CSS to give the application a more native feel; renderer.js starts out completely empty, but we add to it as the chapter goes on.
// ...Omitted for brevity...
body > div {
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch; 1
}
.container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
textarea, input, div, button { outline: none; }
// ...Omitted for brevity...
.clipping-text::-webkit-scrollbar {
display: none; 2
}
.clipping-controls {
margin-top: 0.5em;
}
// ...Omitted for brevity...
With these files in place, we now have the foundation for building our application. menubar does not create a window immediately. Instead, it creates a BrowserWindow instance the first time the user clicks the icon and triggers the window. This behavior comes back to bite us later in the chapter when we try to manipulate the DOM before it has been loaded, but let’s follow the happy path for now by listening for an event that is fired after the window has been created and subsequently loading the HTML in the newly created window.
menubar.on('after-create-window', () => { 1
menubar.window.loadURL(`file://${__dirname}/index.html`);
});
Your application at this stage should look like figure 10.3. At this point, the button should not work, because we have not written any JavaScript for the frontend.

We have a UI for our application, but it doesn’t do anything yet. Just like in chapter 9, we first want to allow the user to add clippings to the application. This is slightly more complicated than it was in the previous chapter because we must generate DOM nodes for the clipping. We need to add this new node to the node in our Markup that contains the list of clippings. Finally, we need to add an event listener to the button to call the two methods we just described. Let’s start by querying and caching two of the selectors that we use frequently as we move through the chapter.
const clippingsList = document.getElementById('clippings-list');
const copyFromClipboardButton = document.getElementById(
'copy-from-clipboard'
);
Creating the element is straightforward, with one important catch. In listing 10.6, we create an article element and add the .clippings-list-item class to it so that it’s styled appropriately. Next, we set its inner content. We query for the node in charge of displaying the text of the clipping and set its inner text accordingly. Finally, we return our new element so that it can be added to the DOM.
const createClippingElement = (clippingText) => {
const clippingElement = document.createElement('article'); 1
clippingElement.classList.add('clippings-list-item');
clippingElement.innerHTML = ` 2
<div class="clipping-text" disabled="true"></div>
<div class="clipping-controls">
<button class="copy-clipping">→ Clipboard</button>
<button class="publish-clipping">Publish</button>
<button class="remove-clipping">Remove</button>
</div>
`;
clippingElement.querySelector('.clipping-text').innerText = clippingText;3
return clippingElement; 4
};
You might be wondering why I didn’t use interpolation with the template literal when setting the inner HTML of the element. We’re not setting the content of the new element using innerHTML, because it does not escape the input and render it as HTML. If a user had copied HTML to the clipboard, it would render the Markup, which is not what we want. Instead we use innerText to set the content of that node, which escapes any HTML and renders it as the user might expect.
We can now take an arbitrary string of text and return the element needed for the UI. The next step is to get the text to feed this function, take the result, and add it to the page.
const { clipboard } = require('electron'); 1
const addClippingToList = () => {
const clippingText = clipboard.readText(); 2
const clippingElement = createClippingElement(clippingText); 3
clippingsList.prepend(clippingElement); 4
};
copyFromClipboardButton.addEventListener('click', addClippingToList); 5
Reading from the system’s clipboard is as easy as requiring the clipboard module from Electron and calling its readText() method. With the clipping text, we create the element and then add it to the top of the list of clippings. Last, we add an event listener to the Copy from Clipboard button that triggers this entire process.
Users can now save clippings to Clipmaster 9000, but that is only half the battle. What if they want to write the contents of a clipping back to the clipboard? Technically, because this new version of the application has a UI, they could select the text and copy it again, but we can do better than that. In addition, Clipmaster 9000 will be an improvement over the old version by letting users remove clippings they no longer want to store and publish them on the web to share publicly if they desire.
The most obvious approach to implementing the functionality described in the previous section would be to add an event listener to each button that calls the appropriate function. When creating a new element for each clipping using createClipping-Element(), we could add these event listeners.
The problem here is that if a user removes a clipping, we also need to remove these event listeners. Failing to do so would cause a memory leak, because the event listener and the DOM element would still reference each other. It’s much easier to take advantage of the fact that events bubble up the DOM.
If you click a button, the browser checks that element for any event listeners for a click event. Next it checks that element’s parent. It will continue this process until it gets to the top of the DOM tree. By adding an event listener to the clippingsList, we can catch an event that originated for a particular clipping. Because the list itself will never be removed from the DOM, we don’t need to worry about removing event listeners whenever a clipping is removed from the UI.
All event objects in the DOM come with a target property, which contains a reference to the element that triggered the event. We look at this to figure out which button was clicked on which clipping and then take the appropriate action.
clippingsList.addEventListener('click', (event) => { 1
const hasClass = className =>
event.target.classList.contains(className); 2
if (hasClass('remove-clipping')) console.log('Remove clipping'); 3
if (hasClass('copy-clipping')) console.log('Copy clipping');
if (hasClass('publish-clipping')) console.log('Publish clipping');
});
We check the element for three different classes, so it makes sense to create a small helper method called hasClass() to aid in this process. Based on the class, we need to call different functions. We haven’t written those functions yet, so we’ll simply log to the console for now to confirm that everything works as it should.
To remove a clipping, we navigate up to its grandparent, which is the element for the entire clipping.
const removeClipping = (target) => {
target.parentNode.parentNode.remove(); 1
};
When we have a reference to the element for the clipping, removing it from the page is as simple as calling the element’s remove() method.
clippingsList.addEventListener('click', (event) => {
const hasClass = className => event.target.classList.contains(className);
if (hasClass('remove-clipping')) removeClipping(event.target); 1
if (hasClass('copy-clipping')) console.log('Copy clipping');
if (hasClass('publish-clipping')) console.log('Publish clipping');
});
Instead of logging to the console whenever the user clicks a clipping’s Remove button, we call removeClipping() and pass it a reference to the button. That works, but we can do a little bit better. Looking ahead, it’s safe to assume that we’ll need to get either a reference to the clipping element or its text. It makes sense to pull these out into their own functions.
const getButtonParent = ({ target }) => { 1
return target.parentNode.parentNode;
};
const getClippingText = (clippingListItem) => { 2
return clippingListItem.querySelector('.clipping-text').innerText;
};
getButtonParent() can navigate to the parent from any of the three buttons. Although simple, this function is useful in case we ever update the Markup. You don’t want to have to change the code for traversing up the DOM from a button to the clipping element three times. From the parent, we need to get the text of the clipping because we’re effectively using the DOM as our data store. Luckily, traversing down the DOM is easier than traversing up, and getClippingText() can take advantage of the querySelector() method. We can now use this method in our event listener.
clippingsList.addEventListener('click', (event) => {
const hasClass = className =>
event.target.classList.contains(className);
const clippingListItem = getButtonParent(event); 1
if (hasClass('remove-clipping')) removeClipping(clippingListItem); 2
if (hasClass('copy-clipping')) console.log('Copy clipping',
getClippingText(clippingListItem));
if (hasClass('publish-clipping')) console.log('Publish clipping',
getClippingText(clippingListItem));;
});
In the previous listing, we immediately get a reference to the clipping element because we’ll need it in every case. Now that getting the clipping text is easy, let’s update our console logs to include the clipping text as well to verify that we’ve implemented this functionality correctly. removeClipping() can now get a lot simpler as well. It’s arguable that we don’t need a function for this at all, but it’s conceivable that we might want to add more functionality later. As a result, it makes sense to leave it for now.
const removeClipping = (target) => {
target.remove(); 1
};
Not only did we implement the ability to write to the clipboard in the previous chapter, we’ve already laid a lot of the groundwork in this chapter to make implementing this feature easy. The first thing that we need is a function that handles writing the text of the clipping to the clipboard.
const writeToClipboard = (clippingText) => { 1
clipboard.writeText(clippingText);
};
We add more to this function later in the chapter, so it makes sense to keep it as a function instead of just adding inline in our event listener. The next step is to replace the console log in the event listener with this new function and pass the function the text of the clipping that was selected by the user.
clippingsList.addEventListener('click', event => {
const hasClass = className => event.target.classList.contains(className);
const clippingListItem = getButtonParent(event);
if (hasClass('remove-clipping')) removeClipping(clippingListItem);
if (hasClass('copy-clipping'))
writeToClipboard(getClippingText(clippingListItem)); 1
if (hasClass('publish-clipping'))
console.log('Publish Clipping', getClippingText(clippingListItem));
});
We now have the core functionality that any self-respecting application that calls itself Clipmaster 9000 would need to do its job. It’s time to start going for extra credit. Let’s implement the ability to publish clippings to an example API, as shown in figure 10.4. We cannot do this in a browser for the same security reasons we discussed in chapter 2. In addition, we use a Node library that would not normally work in the browser to send our requests to the API.

In the name of focus, we’ll take a few shortcuts. This is a deliberatively simple API—it stores the clippings in memory. All of the clippings will be cleared out periodically. The source code for the server can be found at https://glitch.com/~cliphub.
request is another popular, well-named library that makes it easy to perform HTTP requests to remote servers. request allows us to set defaults for every request that it makes. For this application, we send all our requests to the same API endpoint, so it makes sense to set that as a default. We will also create a custom user agent string that will differentiate Clipmaster from an ordinary browser.
In this section, we set up request to make requests to the ClipHub API, format our clippings so that the ClipHub API will accept those requests, and then set up our UI to make the requests.
const request = require('request').defaults({ 1
url: 'https://cliphub.glitch.me/clippings',
headers: { 'User-Agent': 'Clipmaster 9000' }, 2
json: true, 3
});
Now we can format the text of a clipping for the API and send an HTTP request to the API. The next step is to tie these two functions together and show the user whether the request was successful, along with the URL of the clipping on ClipHub if it was successful.
const publishClipping = (clipping) => {
request.post({ json: { clipping } }), (error, response, body) => { 1
if (error) { return alert(JSON.parse(error).message); } 2
const url = body.url; 3
alert(url); 4
clipboard.writeText(url); 5
});
};
request.post() sends a POST request to a URL. We set the default URL earlier, so there is no need to specify it now. request.post() takes two arguments: the data we want to send and a callback that is invoked when we hear back from the server. request passes three arguments to the callback: an error object in the event the request is not successful, the full HTTP response, and the body of the response. If the request was successful, error is null.
If the request is successful, we get the url property, which contains the URL for our new published clipping. For now, we use an alert to display the URL. We also write it to the clipboard so that the user can paste it into the address bar of their favorite web browser. With the code for this feature in place, the last thing to do is call it when the user clicks the Publish button on a clipping.
clippingsList.addEventListener('click', (event) => {
const hasClass = className => event.target.classList.contains(className);
const clippingListItem = getButtonParent(event);
if (hasClass('remove-clipping')) removeClipping(clippingListItem);
if (hasClass('copy-clipping'))
writeToClipboard(getClippingText(clippingListItem));
if (hasClass('publish-clipping'))
publishClipping(getClippingText(clippingListItem)); 1
});
In this listing, we used a technique similar to writing the clipping’s text to the clipboard. The only difference is that we swap out writeToClipboard() in favor of publish-Clipping().
We let the user know that something has happened in Clipmaster in various ways. We silently write to the clipboard without informing the user as to whether the action ended in success. When publishing we use an alert that locks up the application until the user dismisses it.
Later in this chapter, we implement global shortcuts that allow the user to trigger the application’s functionality without having it open. In this situation, having useful notifications is even more important. Displaying these notifications is simple because we do all the heavy lifting in the renderer process and don’t have to worry about IPC. With that in mind, we look at how to add event handlers to our notifications to add functionality that wasn’t present in the previous chapter.
Let’s start by tackling the alert that pops up whenever the user publishes a clipping. In its place, we display one of two notifications, shown in listing 10.19: that an error occurred with the message received from the server, or that the request was successful with the URL of the newly published clipping on ClipHub. If the request was successful, we add an event handler to the notification that opens ClipHub in their default browser when the user clicks the notification.
const { clipboard, shell } = require('electron');
// Code omitted for clarity...
const publishClipping = (clippingText) => {
request.post({ json: { clipping } }), (error, response, body) => {
if (error) { 1
return new Notification('Error Publishing Your Clipping', {
body: JSON.parse(error).message
});
}
const url = body.url;
const notification = new Notification( 2
'Your Clipping Has Been Published',
{ body: `Click to open ${url} in your browser.` }
);
notification.onclick = () => { shell.openExternal(url); }; 3
clipboard.writeText(url);
};
};
To open a URL in the user’s default browser, we need to pull in the shell module from Electron. We’ll set the onclick method of the notification to an anonymous function that is triggered when the user clicks the notification.
Asking the user to take their hand off the keyboard and navigate to a small icon in the menu bar or system tray isn’t always optimal. They’re likely typing when they want to create a clipping, and they want the convenience of having a key combination that they can press from anywhere in the operating system to trigger a command inside Clipmaster 9000.
If you remember from the previous chapter, we must register global shortcuts in the main process. In chapter 9, we implemented most of the application’s functionality in the main process, and we could call functions directly. In this chapter, the opposite is true, and we need to set up some IPC. In the following listing, let’s start by registering a shortcut to create a new clipping. We start by having it log to the console for now. In the next step, we implement its functionality.
const { globalShortcut } = require('electron'); 1
const Menubar = require('menubar');
const menubar = Menubar();
menubar.on('ready', function() {
console.log('Application is ready.');
const createClipping = globalShortcut.register('CommandOrControl+!', ()
=> {
console.log('This will eventually trigger creating a new clipping.');
}); 2
if (!createClipping) {
console.error('Registration failed', 'createClipping');
} 3
});
menubar.on('after-create-window', () => {
menubar.window.loadURL(`file://${__dirname}/index.html`);
});
This is similar to what we did in the previous chapter. The important difference is that we’re storing all the clippings in the DOM in the renderer process. As a result, we need to communicate with the renderer process to create the clipping. In Fire Sale, we kept a reference to each window that we created. menubar created a browser window on our behalf and stored it in its window property. Let’s update our global shortcut to send a message to the renderer process whenever the user presses the keystroke to create a new shortcut. We can also register shortcuts for writing clippings back to the clipboard and publishing them to ClipHub.
const createClipping = globalShortcut.register('CommandOrControl+!', () => {
menubar.window.webContents.send('create-new-clipping');
});
const writeClipping = globalShortcut.register('CmdOrCtrl+Alt+@', () => {
menubar.window.webContents.send('write-to-clipboard');
});
const publishClipping = globalShortcut.register('CmdOrCtrl+Alt+#', () => {
menubar.window.webContents.send('publish-clipping');
});
if (!createClipping) {
console.error('Registration failed', 'createClipping');
}
if (!writeClipping) {
console.error('Registration failed', 'writeClipping');
}
if (!publishClipping) {
console.error('Registration failed', 'publishClipping');
}
Each shortcut is sending a message on a different channel. In this case, we send no additional information to the renderer process because the process reads from the clipboard. The next step is to configure the renderer process to receive the messages sent from the main process. The user may not have the application open when they press the keystroke, so we add notifications where appropriate.
const { clipboard, ipcRenderer, shell } = require('electron'); 1
ipcRenderer.on('create-new-clipping', () => {
addClippingToList(); 2
new Notification('Clipping Added', { 3
body: `${clipboard.readText()}`
});
});
ipcRenderer.on('write-to-clipboard', () => {
const clipping = clippingsList.firstChild; 4
writeToClipboard(getClippingText(clipping)); 5
});
ipcRenderer.on('publish-clipping', () => {
const clipping = clippingsList.firstChild;
publishClipping(getClippingText(clipping)); 6
});
In chapter 9, we stored all the clippings in an array in the main process. In this chapter, we’re using the DOM as our temporary data store. If the user clicks the button to write to the clipboard or publish the clipping, we know which clipping, based on the button clicked. But how do we find the appropriate clipping when the user activates a global shortcut? In the previous listing, we traverse to the first child of the clippings list and call one of the functions we wrote earlier in the chapter with that element.
You might have noticed a bug when implementing the last feature. If you start your application and immediately press one of your new global shortcuts, you get an error, shown in figure 10.5, that reads “TypeError: Cannot read property ’webContents’ of undefined.” If you look at the previous listing, you’ll notice that we’re attempting to access the webContents property on menubar.window, which is apparently undefined. Click the cat icon to open the window and try the shortcut again. It should work this time.

So why does it suddenly work? menubar lazily loads the window the first time it is needed. To prevent this error, we must tell menubar to immediately load the window when it starts up. We also want it to load our HTML page, which—in turn—loads renderer.js, which sets up our IPC listeners.
const menubar = Menubar({
preloadWindow: true, 1
index: `file://${__dirname}/index.html`, 2
});
Previously, we were invoking the Menubar() function with no arguments. In listing 10.23, we modified the function call and passed in a configuration object. We preload the window before the first time the user clicks on the menu bar or tray icon and we load in our HTML page as soon as menubar loads the window.
In Fire Sale, I boasted that one of the great things about building applications in Electron is that we could use application and context menus to provide functionality without needing to find a place for it in the UI. In the previous chapter, we could have created additional submenus if necessary. But what about Clipmaster 9000? It turns out that the tray module allows us to display a secondary menu when the user right-clicks the icon. Let’s create a simple menu that allows users to quit the application. We accomplish this in three steps: create the menu using Menu.buildFromTemplate(), add an event handler for right-clicks on menubar.tray, and pop up the menu when the user right-clicks the icon.
const { globalShortcut, Menu } = require('electron'); 1
const secondaryMenu = Menu.buildFromTemplate([ 2
{
label: 'Quit',
click() { menubar.app.quit(); }, 3
accelerator: 'CommandOrControl+Q'
},
]);
menubar.on('ready', function () {
console.log('Application is ready.');
menubar.tray.on('right-click', () => { 4
menubar.tray.popUpContextMenu(secondaryMenu); 5
});
// Omitted for brevity...
});
With this small change, we create an entire secondary interface to the application. This would be a great place for user preferences and other advanced options that don’t have a good place in the main UI of the application.
The first phase of Clipmaster 9000 is now complete. You can find the complete code in the appendix or on the completed-example branch of this repository (http://mng.bz/UE9).