Have you ever wondered if you can build cross platform desktop apps with HTML, CSS and JavaScript?

It is possible with Electron.

This article is a step-by-step guide that will help you understand some core concepts of Electron.

By the end of this post, you’ll know the process of creating cross platform desktop apps with Electron, HTML and CSS.

Before we get started you can check out in advance the app we’re going to build in this tutorial.

Hear Me Type, as I named it, will have a simple but straightforward functionality: for every keyboard key pressed play a specific sound related to that key.

So if I press the “A” button, the app will play the sound of the letter A.

Hear Me Type Tutorial app GitHub repository

Hear Me Type GitHub repository (a more advanced version of the app. Recommended for more experienced Electron users)

The apps’ code will change once in a while as I’m adding new features and enhancements so be sure to check out the commits to see what’s new.

Without further ado, let’s find out more about Electron and create our first app!

What is Electron?

Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It uses Chromium and Node.js so you can build your app with HTML, CSS, and JavaScript similar to a web-based application.

Your newly created app will be compatible with Mac, Windows and Linux operating systems.

Other in-built features are:

Automatic updates – enable apps to automatically update themselves

– enable apps to automatically update themselves Native menus and notifications – creates native application menus and context menus

– creates native application menus and context menus App crash reporting – you can submit crash reports to a remote server

– you can submit crash reports to a remote server Debugging and profiling – it uses Chromium’s content module for finding performance bottlenecks and slow operations. You can also use your favorite Chrome Developer Tools within your app if you’d like

– it uses Chromium’s content module for finding performance bottlenecks and slow operations. You can also use your favorite Chrome Developer Tools within your app if you’d like Windows installer – you can create install packages. Fast and simple.

Getting Started

If you’re happy with what Electron offers, let’s dive deeper and create a simple Electron app.

Before we get our hands dirty, make sure you have the dependencies like Node.js installed (you can download it from here) and also a GitHub account where you can store your app and make changes to it.

Now that Node.js is installed and a GitHub account, open your Bash or Terminal and follow the instructions below to clone the Electron Quick Start Git repository to your computer. We’re gonna build our software upon this useful but minimal app:

# Clone the Quick Start repository

$ git clone https://github.com/electron/electron-quick-start

# Go into the repository

$ cd electron-quick-start

# Install the dependencies and run

$ npm install && npm star t

When you have completed the steps above you should see that our app opens up in a window that looks like a browser’s window. And it is indeed a browser window!

The app’s window style changes depending on the Operating System’s default or customized window style. I chose to use the classic look of Windows. Groovy!

Like I was saying earlier, you can use Chrome’s Developer Tools inside your app. What you can do on your browser’s Developer Tools, can also be done inside the app. Outstanding!

Let’s have a look at the source code and the file structure of our app. Open up the project into your favorite text editor or IDE. I’m going to use Atom which is built on… you guessed it… Electron!

We have a basic file structure:

electron-quick-start

- index.html

- main.js

- package.json

- render.js

The file structure is similar to the one we use when creating web pages.

We have:

index.html which is simply an HTML5 web page and it serves one big purpose: our canvas

which is simply an HTML5 web page and it serves one big purpose: our canvas main.js creates windows and handles system events

creates windows and handles system events package.json similar to Node.js’ modules, is the startup script of our app, which will run in the main process and it contains information about our app

similar to Node.js’ modules, is the startup script of our app, which will run in the main process and it contains information about our app render.js is responsible for hosting the app’s render processes

You may have a few questions up until now, and one of them might be regarding the main process and render process thingies. What the heck are those little bastards and how can I get along with them?

Glad you asked. Hang on to your hat ’cause this is probably new territory if you’re coming from browser JavaScript realm!

What is a process?

When you say process you think of an operating system level process which is in fact just an instance of a computer program that is being executed.

If I start my Electron app and check the Windows Task Manager/Activity Monitor for macOS, I can see how many processes are associated with my app.

Each of these processes run concurrently to each other but the memory and resources are isolated.

Let’s say I want to create a specific for loop that increments something in one of my render processes.

var a = 1; for ( a = 1; a < 10; a ++) { console.log('This is a for loop'); }

The above incrementation is available only in the render process, it doesn’t affect the main process at all, thus the ‘This is a for loop’ message will appear only on the rendered module.

Main Process

The main process controls the life of the application. It has the full Node.js API built in and it is responsible for opening dialogs and other operating system interactions, creating render processes, and starting and quitting the app.

This process is commonly a file named main.js , like in our case, but it can have whatever name you’d like.

You can also change the main process file by modifying it in package.json file.

Just for testing purpose, change:

"main": "main.js",

to

"main": "mainTest.js",

See if your app still works.

Bear in mind that there can be only one main process.

Render Process

The render process is a browser window in your app. Unlike the main process, there can be multiple render processes and each is independent. Every render process is a separate process, meaning a crash in one won’t affect another. This is thanks to Chromium’s multi-process architecture.

These browser windows can also be hidden and of course, customized because they’re like typical HTML files, but in Electron we’ve also got the full Node.js API in here which means that we can open dialogs and other operating system interactions.

Think of it like this:

One question remains now. Can they both be linked somehow?

While these processes run concurrently and independently to each other, they still need to communicate somehow since they’re both responsible for different tasks.

For this there’s an interprocess communication system, or IPC. You can use IPC to pass messages between main and render processes.

For a more in-depth explanation of this system check Christian Engvall’s ‘IpcMain and IPCRenderer’ article

This pretty much sums up the minimum knowledge about main process and render process you have to know before starting building an Electron application.

Now let’s get back to our code!

Make It Personal

Let’s give our app’s folder a proper name.

Change the folder name from electron-quick-start to hear-me-type-tutorial .

Reopen the folder with your favorite text editor or IDE and let’s further customize our app’s identity by opening up the package.json file.

package.json contains vital information about our app like its name, version, main file, author, license and so much more.

Let’s get a little bit of pride and put yourself as author of the app.

Find the "author" parameter and change it’s value to your name. It should look like this:

"author": "Carol Pelu",

We also need to change the rest of the parameters. Modify the “name” and “description” for now. I find the following name and description appropriate for our app:

Awesome! Now our app has a new name and a short but straight to the point description.

Remember, you can always run npm start in your bash to execute the app and see the changes you’ve made.

Let’s move forward and add the expected functionality of our app. We want to play a specific sound for every keyboard key that we press.

Oh, the Fun-ctionalitee!

What is an app without fun-ctionality? Nothing much…

Now we must take care of it and give our app the functionality it desires.

To make the app react to our input, we must first define an element to hook upon and then trigger the desired action.

In order to do that we will create an audio element with specific id s for the keyboard keys that we want. Then we will create a switch statement to find out which keyboard key has been pressed, then play a specific sound of that key.

If this sound a little complex to you now, have no fear. I will guide you through every step.

Download this archive containing all the sound files we’ll be using. We’ll soon make use of them!

Open up the index.html file and let’s create the <audio> elements in order to embed the sound content in our app.

Inside the <body> element, create a div element with the “audio” class tag.

Then inside the newly created div element, create an <audio> element with an id of “A”, the source tag of “sounds/A.mp3” and with a preload attribute of “auto”.

We’ll use preload="auto" to tell the app that it should load the entire audio file when the page loads. index.html being the main file of the app, all of our sound files will be fully loaded when the app executes.

The code should look like this:

<div class="audio"> <audio id="A" src="sounds/A.mp3" preload="auto"></audio> </div>

Now the <audio> is pointing to an unknown source file. Let’s create a folder called sounds and unzip all the sound files inside the folder.

Great! The only important thing that’s missing right now is the JavaScript code.

Create a new file called functions.js and let’s require it within the index.html file so that the JS code is ready for use when the app is running.

Following the example of require(./renderer.js') , right under it add this line of code:

require('./functions.js')

Your project should look like this:

Outstanding! Now that we have everything stitched up, it’s time for the moment of truth!

Open up the functions.js file and add the following JavaScript code into the file. I’ll explain how it works in just a moment.

document.onkeydown = function(e) { switch (e.keyCode) { case 65: document.getElementById('A').play(); break; default: console.log("Key is not found!"); } };

The code should look like this:



Open your bash in your project’s folder and type npm start to run the app.

Tune up the volume of your speakers and press the A button on your keyboard.

#MindBlown

The JS code is pretty simple and straightforward.

We use the onkeydown event on the document object to find out which HTML element is being accessed within the document object which is in fact our app’s main window.

Within the anonymous function we use a switch statement whose purpose is to identify the Unicode value of the pressed keyboard key.

If the Unicode value of the pressed keyboard key is correct, the sound is played, otherwise throw a “not found” message into the console.

Whfiu! What a ride!

Maybe you’ve noticed that we have sound files to cover A-Z and 0-9 keys. Let’s use them too so they don’t feel the bitter taste of loneliness.

Head over to index.html and create an <audio> element for every key that we have a sound file for.

The code should look like this:

Yeah, of course you can copy-paste:

<audio id="B" src="sounds/B.mp3" preload="auto"></audio> <audio id="C" src="sounds/C.mp3" preload="auto"></audio> <audio id="D" src="sounds/D.mp3" preload="auto"></audio> <audio id="E" src="sounds/E.mp3" preload="auto"></audio> <audio id="F" src="sounds/F.mp3" preload="auto"></audio> <audio id="G" src="sounds/G.mp3" preload="auto"></audio> <audio id="H" src="sounds/H.mp3" preload="auto"></audio> <audio id="I" src="sounds/I.mp3" preload="auto"></audio> <audio id="J" src="sounds/J.mp3" preload="auto"></audio> <audio id="K" src="sounds/K.mp3" preload="auto"></audio> <audio id="L" src="sounds/L.mp3" preload="auto"></audio> <audio id="M" src="sounds/M.mp3" preload="auto"></audio> <audio id="N" src="sounds/N.mp3" preload="auto"></audio> <audio id="O" src="sounds/O.mp3" preload="auto"></audio> <audio id="P" src="sounds/P.mp3" preload="auto"></audio> <audio id="Q" src="sounds/Q.mp3" preload="auto"></audio> <audio id="R" src="sounds/R.mp3" preload="auto"></audio> <audio id="S" src="sounds/S.mp3" preload="auto"></audio> <audio id="T" src="sounds/T.mp3" preload="auto"></audio> <audio id="U" src="sounds/U.mp3" preload="auto"></audio> <audio id="V" src="sounds/V.mp3" preload="auto"></audio> <audio id="W" src="sounds/W.mp3" preload="auto"></audio> <audio id="X" src="sounds/X.mp3" preload="auto"></audio> <audio id="Y" src="sounds/Y.mp3" preload="auto"></audio> <audio id="Z" src="sounds/Z.mp3" preload="auto"></audio> <audio id="0" src="sounds/0.mp3" preload="auto"></audio> <audio id="1" src="sounds/1.mp3" preload="auto"></audio> <audio id="2" src="sounds/2.mp3" preload="auto"></audio> <audio id="3" src="sounds/3.mp3" preload="auto"></audio> <audio id="4" src="sounds/4.mp3" preload="auto"></audio> <audio id="5" src="sounds/5.mp3" preload="auto"></audio> <audio id="6" src="sounds/6.mp3" preload="auto"></audio> <audio id="7" src="sounds/7.mp3" preload="auto"></audio> <audio id="8" src="sounds/8.mp3" preload="auto"></audio> <audio id="9" src="sounds/9.mp3" preload="auto"></audio>

Awesome! Now let’s do the same thing for the JS code within functions.js .

You can find the char codes (key codes) on this website.

But yeah, you can copy-paste this too:

document.onkeydown = function(e) { switch (e.keyCode) { case 48: document.getElementById('0').play(); break; case 49: document.getElementById('1').play(); break; case 50: document.getElementById('2').play(); break; case 51: document.getElementById('3').play(); break; case 52: document.getElementById('4').play(); break; case 53: document.getElementById('5').play(); break; case 54: document.getElementById('6').play(); break; case 55: document.getElementById('7').play(); break; case 56: document.getElementById('8').play(); break; case 57: document.getElementById('9').play(); break; case 65: document.getElementById('A').play(); break; case 66: document.getElementById('B').play(); break; case 67: document.getElementById('C').play(); break; case 68: document.getElementById('D').play(); break; case 69: document.getElementById('E').play(); break; case 70: document.getElementById('F').play(); break; case 71: document.getElementById('G').play(); break; case 72: document.getElementById('H').play(); break; case 73: document.getElementById('I').play(); break; case 74: document.getElementById('J').play(); break; case 75: document.getElementById('K').play(); break; case 76: document.getElementById('L').play(); break; case 77: document.getElementById('M').play(); break; case 78: document.getElementById('N').play(); break; case 79: document.getElementById('O').play(); break; case 80: document.getElementById('P').play(); break; case 81: document.getElementById('Q').play(); break; case 82: document.getElementById('R').play(); break; case 83: document.getElementById('S').play(); break; case 84: document.getElementById('T').play(); break; case 85: document.getElementById('U').play(); break; case 86: document.getElementById('V').play(); break; case 87: document.getElementById('W').play(); break; case 88: document.getElementById('X').play(); break; case 89: document.getElementById('Y').play(); break; case 90: document.getElementById('Z').play(); break; default: console.log("Key is not found!"); } };

Our app is now complete! Congrats!

The main functionality of the app is done, but there is still work to be done!

Polska ja! (Polish me!)

Even though the app is functional it still lacks some things here and there.

For example, within the index.html file you can change the app’s title and the content that you want to be shown in the main window.

Moreover, the app has no design, no beautiful colors, no pictures with either cats or dogs.

Give freedom to your imagination and find out ways you can improve the app’s design!

The code isn’t perfect either. We have lots of identical code which can be optimized and improved in less lines of code and less painful for the eyes. Code duplication is bad practice!

Test It! Just Test It!

A good software must be thoroughly tested.

I’d suggest to begin with pressing every keyboard key to see what’s happening.

Best scenario is you will hear the audio for every keyboard key you have specified in the code, but who knows what will happen when you press multiple keys in a row as fast as you can or keys that are not even supposed to be pressed like the Home and NumLock buttons.

What happens when you minimize the app and try to press a key? Do you hear a sound? But what happens when you don’t have the app window selected and you press a keyboard key, do you still hear any sounds?

The answer is unfortunately a no.

This behavior is because of the architecture upon which Electron was built. It does allow you to get global keys, like you can do with the C# language, but you can’t register individual keystrokes. This is outside of the realm of normal use-cases for an electron application. I don’t know the real reason behind this caveat but I guess it has something to do with keyloggers and user’s privacy.

I want you to grab the code line by line and break it to see what is happening and what kind of errors is Electron throwing out. This exercise will help you become better at debugging. If you know the flaws of your app you then know how to fix them and make the app better.

In functions.js file I have used a deprecated JavaScript event.

Once you find it I would like you to think about how you can replace this deprecated event without changing the functionality of the app.

Using deprecated code is bad practice and can lead to serious bugs you might not even know they exist. It’s recommended to read again the documentation of the language to see what has changed and stay up to date.

Conclusion

First of all I would like to thank and congratulate you for reaching this point!

You now have the knowledge to create a simple cross-platform Electron app.

If you want to dive deeper into Electron and see what I am working on make sure to check out Hear Me Type and my profile on GitHub.

Feel free to clone, fork, star and contribute to any of my public projects.

Last but not least, I’d suggest you to come back and read again this article from time to time as I will constantly update it according to what changes Electron will suffer over time.