How to use the Mac TouchBar API in Electron
The TouchBar at the top of the MacBook Pro keyboard changes based on the application you’re using and what you’re doing. It provides intuitive shortcuts and controls. For instance, when you select an input field, TouchBar will display a set of emojis. I think this is a cool feature — I can change the screen brightness or open terminal in Visual Studio Code real quick.
If you are developing a native application for macOS you might be interested in the NSTouchBar object, which provides dynamic contextual controls in the Touch Bar of supported models of MacBook Pro. Unfortunately, no Web API supports TouchBar yet that we could use on our website — that makes kind of sense because I cannot imagine ads appearing on my TouchBar. When it comes to Electron, things are a little bit different. If you develop a desktop application with frameworks like Electron you might want to give it as much native app feeling as you can. Electron comes with a module called TouchBar and using it is easier than you think!
DISCLAIMER: As of March 2020 the TouchBar API is experimental and it may not work as
expected. If you are curious about current state of development this module or would like to help, I encourage you
to check out the source code on Electron’s GitHub in ./lib/browser/api/touch-bar.js:
https://github.com/electron/electron/blob/v9.0.0-beta.9/lib/browser/api/touch-bar.js
Prerequisites
Before we begin we need to set up an Electron project first (obviously). This step is pretty straight forward:
- Make a new directory, run npm init inside. Your app starting point/initial file should be app.js or index.js.
- Install the electron with the command:
npm i electron
- And add start script inside of your package.json:
- The last thing is to write some code inside of app.js and index.html and we are good to go!
{
"name": "your-app",
"version": "0.1.0",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
const { app, BrowserWindow } = require('electron')
function createWindow () {
// Create the browser window.
let win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
<!DOCTYPE html>
<html>
<head>
<Data charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<h1>Hello World!</h1>
We are using node <script>document.write(process.versions.node)</script>,
Chrome <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
</body>
</html>
Type npm run start in terminal to see if everything was set up properly.
Electron TouchBar controls
I’m going to focus on the most tricky parts of this API and hacks that you can do to achieve more fancy results. That means I will not describe all the props of each TouchBar item object. You can view all controls in Electron docs. There is also a cool example that I encourage you to try out:
TouchBarButton
Let’s start with something easy — a counter.
We need to import TouchBar object to our app first:
const { app, BrowserWindow, TouchBar } = require('electron');
const { TouchBarButton } = TouchBar;
Initialize Touch bar with:
const touchBar = new TouchBar({
items: [],
});
And set it to the window inside of our createWindow() function:
win.setTouchBar(touchBar);
Create a new button, write some logic and add it to items[] inside touchBar object.
WARNING: Each element must be unique! E.g. exact same looking button must be declared
again with different variable name. Because this module is experimental, you won’t get any error if you duplicate
same item inside items array.
Final code:
let counter = 0;
const update = () => {
counter += 1;
button.label = `Count: ${counter}`;
};
const button = new TouchBarButton({
label: `Count: ${counter}`,
accessibilityLabel: 'Counter',
backgroundColor: '#6ab04c',
click: () => {
update();
}
});
const touchBar = new TouchBar({
items: [
button,
],
});
Start your application. You should achieve something like this:
TouchBarButton with an image
The title is self-explanatory, but how do we add an image to the TouchBar component? The trick is to use nativeImage module:
nativeImage documentationI have downloaded Medium logo in 1000x1000 size and using mentioned above module resized it down to 30px, which is the max height of the TouchBar. In this example, I rewrote a code a bit so I can access the win object. That will allow me to set an URL of the Electron app on button click.
Code:
const {
app, BrowserWindow, TouchBar, nativeImage,
} = require('electron');
const { TouchBarButton } = TouchBar;
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
let counter = 0;
const image = nativeImage.createFromPath('./m.png').resize({ height: 30 });
const update = () => {
counter += 1;
button.label = `Count: ${counter}`;
};
const button = new TouchBarButton({
label: `Count: ${counter}`,
accessibilityLabel: 'Counter',
backgroundColor: '#6ab04c',
click: () => {
update();
},
});
const button2 = new TouchBarButton({
icon: image,
iconPosition: 'left',
label: 'How to use TouchBar API in ElectronJS?',
accessibilityLabel: 'Button looking like a label',
backgroundColor: '#000',
click: () => {
win.loadURL('https://medium.com/p/c58914a5518a');
},
});
const touchBar = new TouchBar({
items: [
button,
button2,
],
});
win.loadFile('index.html');
win.setTouchBar(touchBar);
});
Result:
It’s worth mentioning that there is no nativeImage width limit. Combining the image sequence with setTimeouts will give you some interesting results!
How to integrate TouchBarSlider with our Electron HTML?
For this, we are going to send our slider value using webContents.send() and receive a message from the main process using ipcRenderer which “provides a few methods so you can send synchronous and asynchronous messages from the render process (web page) to the main process. You can also receive replies from the main process.” If you ever tried socket.io, then this one will be easy for you to understand.
In this example, I will try to show slider values inside index.html. TouchBarSlider will use maximum bar width so if you want to make it shorter use TouchBarSpacer.
Code:
const {
app, BrowserWindow, TouchBar,
} = require('electron');
const { TouchBarSlider } = TouchBar;
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const slider = new TouchBarSlider({
label: 'Simple slider',
value: 10, // initial value
minValue: 0,
maxValue: 100,
change: (value) => {
console.log(value); // print value inside terminal
win.webContents.send('slider', value);
},
});
const touchBar = new TouchBar({
items: [
slider,
],
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<input type="range" min="1" max="100" value="10" class="slider" id="myRange">
<script>
const ipc = require('electron').ipcRenderer;
const slider = document.getElementById('myRange');
ipc.on('slider', (event, message) => {
slider.value = message;
console.log(message);
})
</script>
</body>
</html>
TouchBarGroup, TouchBarSpacer & TouchBarLabel
TouchBarSpacer will create some space between items for us. TouchBarGroup, as its name says, creates a group of nested elements. I didn’t found a good usage for this object but in case you did here’s the code snippet for you — this one is tricky (at least was for me) because you have to initialize one more TouchBar instance:
const {
app, BrowserWindow, TouchBar,
} = require('electron');
const { TouchBarGroup, TouchBarSpacer, TouchBarLabel } = TouchBar;
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const group = new TouchBarGroup({
items: new TouchBar({
items: [
new TouchBarLabel({ label: 'group' }),
new TouchBarLabel({ label: 'group', textColor: '#eb4d4b' }),
new TouchBarSpacer({ size: 'large' }), // this will not work
new TouchBarLabel({ label: 'group' }),
],
}),
});
const touchBar = new TouchBar({
items: [
group,
new TouchBarSpacer({ size: 'large' }),
new TouchBarLabel({ label: '<- large' }),
new TouchBarSpacer({ size: 'flexible' }),
new TouchBarLabel({ label: '<- flexible takes all space' }),
],
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
});
It is worth noting that TouchBarSpacer won’t affect anything inside TouchBarGroup (bug?).
Color picker using TouchBarColorPicker:
This will render color palette button that opens default color picker with “featured” colors:
const touchBar = new TouchBar({
items: [
new TouchBarColorPicker({
availableColors: ['#f9ca24', '#f0932b', '#eb4d4b', '#6ab04c', '#c7ecee'],
selectedColor: '#6ab04c',
change: (color) => {
console.log(`Selected color: ${color}`);
},
}),
],
});
What is TouchBarSegmentedControl?
I think we could compare this to a select tag in HTML. The object itself gives us a lot of options: we can define disabled labels, buttons, and multi-choice groups. It will accept nativeImage icon too. Be careful though — adding too many options will hide the last segment:
const {
app, BrowserWindow, TouchBar, nativeImage,
} = require('electron');
const { TouchBarSegmentedControl } = TouchBar;
const image = nativeImage.createFromPath('./m.png').resize({ height: 30 });
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const touchBar = new TouchBar({
items: [
new TouchBarSegmentedControl({
segmentStyle: 'automatic',
segments: [
{ icon: image },
{ icon: image, label: 'icon with text' },
{ label: 'baz', enabled: false },
{ label: 'bar' },
],
selectedIndex: 3,
change: (selectedIndex, isSelected) => {
console.log(selectedIndex, isSelected);
},
}),
new TouchBarSegmentedControl({
segmentStyle: 'rounded',
mode: 'multiple',
segments: [
{ label: 'multiple' },
{ label: 'choice' },
],
selectedIndex: 1,
}),
new TouchBarSegmentedControl({
segmentStyle: 'automatic',
mode: 'buttons',
segments: [
{ label: 'can't be' },
{ label: 'selected' },
],
change: (selectedIndex) => {
console.log(selectedIndex);
},
}),
],
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
});
TouchBarPopover
This one is interesting. It works similarly to color picker — renders a button that will open “new touch bar” (or nested one):
const {
app, BrowserWindow, TouchBar, nativeImage,
} = require('electron');
const { TouchBarPopover, TouchBarButton } = TouchBar;
const image = nativeImage.createFromPath('./m.png').resize({ height: 30 });
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const touchBar = new TouchBar({
items: [
new TouchBarPopover({
icon: image,
showCloseButton: true,
items: new TouchBar({
items: [
new TouchBarButton({ label: 'pop' })],
}),
}),
new TouchBarPopover({
label: 'Second popover',
showCloseButton: true,
items: new TouchBar({
items: [
new TouchBarButton({ label: 'pop' })],
}),
}),
],
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
});
TouchBarScrubber…
… is a scrollable select menu. ScrubberItem[] accepts icon property, so using this object we can create an image gallery. Besides, I will replace the Escape button to not waste more Gist space 😅
const {
app, BrowserWindow, TouchBar, nativeImage,
} = require('electron');
const { TouchBarScrubber, TouchBarButton } = TouchBar;
const image = nativeImage.createFromPath('./m.png').resize({ height: 30 });
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const button = new TouchBarButton({
label: 'Emoji works aswell 🦁',
backgroundColor: '#7851A9',
click: () => {},
});
const touchBar = new TouchBar({
items: [
new TouchBarScrubber({
items: [{ label: 'foo' }, { label: 'bar' }, {
icon: image,
}],
selectedStyle: 'outline',
mode: 'free',
showArrowButtons: true,
}),
],
escapeItem: button,
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
});
Dynamic images? Why not?!
At this point, you may think that TouchBar API is a little bit limited. If you mix HTML Canvas API with nativeImage you will achieve more “professional” feeling. To do so, we need to generate BLOB out of HTML Canvas in our renderer and send buffer to nativeImage to finally attach it to our bar:
const {
app, BrowserWindow, TouchBar, nativeImage, ipcMain,
} = require('electron');
const { TouchBarScrubber, TouchBarButton } = TouchBar;
const image = nativeImage.createFromPath('./m.png').resize({ height: 30 });
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
});
const button = new TouchBarButton({
backgroundColor: '#eb4d4b',
click: () => {
win.loadURL('https://github.com/wisniewski94');
},
});
const touchBar = new TouchBar({
escapeItem: button,
});
win.loadFile('index.html');
win.webContents.openDevTools();
win.setTouchBar(touchBar);
ipcMain.on('newImage', (event, arg) => {
button.label = '';
button.icon = nativeImage.createFromBuffer(arg).resize({ height: 23 });
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body>
<canvas id="canvas" width="100" height="100" style="display: none"></canvas>
<script>
const { nativeImage, ipcRenderer } = require('electron');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = './glyph.png'
img.onload = () => {
canvas.width = 200;
canvas.height = 30;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 30, 30);
ctx.font = "12px Roboto";
ctx.fillStyle = 'white';
ctx.fillText("Check out my GitHub profile:", 40, 11);
ctx.font = "600 16px Roboto";
ctx.fillText("wisniewski94", 40, 29);
const data = canvas.toDataURL('image/png', 1);
const image = nativeImage.createFromDataURL(data).toPNG();
ipcRenderer.send('newImage', image);
}
</script>
</body>
</html>
This is just proof of concept. The generated image is blurry and requires more processing. Don’t worry, blurriness is related to the canvas itself than TouchBar. Technically to receive better image quality, you can try converting HTML SVG to BLOB as well.
Conclusion
In this article, I covered (I believe) all aspects of Electron’s TouchBar API. I hope that in the future this functionality will get more features, e.g. some programs render a keyboard piano or interactive timeline for video editing. Because this is still in the experimental phase a lot can change and if it does and you will find this article outdated, please send me a DM on Twitter so I can update this tutorial 🙂. Thanks!