How to Add Keyboard Shortcuts to Your Web App.
Imagine a scenario. You work for a large corporation called Adob... you work at a startup called Abode developing a photo editing application called PhotoStore. It sounds like an image storing service, but it is not! In fact, this is a web-based alternative to a popular desktop application.
One day, your product manager announces that since your competitor has keyboard shortcuts, you need them too to make switching easier for their existing users. After all, this should not delay our launch. You will just add document.addEventListener('keydown', callback);
and you are done, right?
Well, not so fast. Why do we need shortcuts? In our case, we want to be able to activate various tools in the toolbar (think Brush or Selection). But even in our simple use case, we can reasonably expect the need to handle the following.
- activate the selected tool (state management)
- highlight the selected tool (CSS styles)
- do everything above only in the editor (shortcuts should not work in marketing pages, guides, etc.)
Before we write a line of code, it is always good to think about how a feature might evolve in the future. We can prevent a lot of pain down the road by designing our solution in a way that supports this theoretical functionality, even if we decide not to implement it at this time.
Why Even Bother?
"This does not apply to me, I am not working on a photo editing application," you think.
While that might be true, users of every application develop habits and adopt certain workflows. And this includes your app. Offering keyboard shortcuts to the most used features makes your users faster, more productive, and as a result, happier.
Sometimes, your users cannot or do not want to use their mouse, making shortcuts almost a necessary part of your product. In those cases, I would even count them as a part of the overall accessibility strategy. And you do care about that, correct? That's what I thought. So let's get started.
Prerequisites.
What do we need to deliver this delightful experience? For this article, I will use "only" React with JSX, allowing me to write compact code. I put only in quotes because setting up React can be daunting if you never worked with it before.
However, if you are adding shortcuts to your application, chances are, you already use some SPA framework. What we need are the onCreate()
and onDestroy()
lifecycle methods, so if your framework offers an equivalent, this guide will be easy to follow. In React, these methods are called componentDidMount()
and componentWillUnmount()
.
Don't panic if you use a vanilla JavaScript, you can still follow along. We will be focusing on the concepts, not a particular implementation. But you should be able to read ES6, because I am not about that verbose code life.
Project Setup.
I managed to get ahold of the Abode PhotoStore application and published it to CodePen so you can get started quickly and follow along. (Hopefully, Abode won't sue us for leaking their product.)
As you can see, I obtained an early version of their system. They have not made a design hire yet and there are only 2 tools to choose from - Brush and Select. We can also switch between pages. That's handy, since we will want to ensure our shortcuts work only in the editor. So for our needs, this will be enough.
See the Pen Shortcuts Tutorial - Step 1 by Lubos (@mrlubos) on CodePen.
Concepts.
How will this feature work? We will need to create the following.
- Available shortcuts. This will be a list of shortcuts we can call. They will map to an object containing all application shortcuts.
- Listeners. We want to ensure shortcuts work only in the editor. Our framework's lifecycle methods will come handy here.
- Handlers. Functions processing keyboard events and actual shortcut execution.
Implementation.
Let's begin by defining app shortcuts. This will be an object of all shortcuts that can be executed in the app. For now, we will only be able to select the tools from our toolbar. Every shortcut handler will be a function accepting a KeyboardEvent object as a single parameter. It checks if the event matches the trigger pattern before executing its body.
const shortcuts = {
toolBrush(event) {
if (event.key !== 'b') return false;
useTool('brush');
return true;
},
toolSelect(event) {
if (event.key !== 'm') return false;
useTool('select');
return true;
},
};
To satisfy our requirement of being able to execute only certain shortcuts on certain pages, we will have a list of currently registered shortcuts.
let registeredShortcuts = [];
When user presses a key, we will iterate through this list and see if the key event executes any shortcut, at which point we stop iterating. If we looped through the shortcuts
keys instead, it would be much harder to restrict the shortcut execution to a specific page(s).
const handleKeyDown = event => {
for (let i = 0; i < registeredShortcuts.length; i += 1) {
const shortcut = registeredShortcuts[i];
if (shortcuts[shortcut](event)) {
event.preventDefault();
break;
}
}
};
To start listening for key presses, we register a global listener.
document.addEventListener('keydown', handleKeyDown);
You can now add 'toolBrush'
and 'toolSelect'
inside the registeredShortcuts
array to see that our shortcuts do indeed get called.
See the Pen Shortcuts Tutorial - Step 2 by Lubos (@mrlubos) on CodePen.
Now that we've verified our shortcuts work, we can clear the array and write a proper <Shortcut />
component that will handle shortcut registration for us. We will wrap our toolbar buttons inside this component.
<Shortcut name="toolBrush">
<Button
label="Brush"
onClick={useTool}
to="brush" />
</Shortcut>
<Shortcut name="toolSelect">
<Button
label="Select"
onClick={useTool}
to="select" />
</Shortcut>
This has a range of advantages. First of all, when we view the code later, it will be obvious which parts of our application this shortcut affects. It will be also easy to search for a particular shortcut inside our project by typing <Shortcut name="searched_name_here">
.
By embedding each shortcut inside a render method of your view, you bind its lifecycle length to the parent component. And using the component lifecycle methods to register and deregister shortcuts makes state management a breeze. When the component gets created, we register the shortcut. Conversely, we deregister the same shortcut on destroy.
I love pure JavaScript as anyone else, but this would be a nightmare to manage. Here is our component.
class Shortcut extends React.Component {
componentDidMount() {
if (!registeredShortcuts.includes(this.props.name)) {
registeredShortcuts = [
...registeredShortcuts,
this.props.name,
];
}
}
componentWillUnmount() {
registeredShortcuts = [
...registeredShortcuts.filter(x => x !== this.props.name),
];
}
render() {
return this.props.children || null;
}
}
Now we need to change our toolbar to be present only in the editor view. Let's also add styling to it and remove the console information. And that's it!
See the Pen Shortcuts Tutorial - Step 3 by Lubos (@mrlubos) on CodePen.
Addendum.
Works like a charm, right? We have successfully added a support for keyboard shortcuts to our app. Are we ready to compete with our nemesis now? Totally. The marketing team can start promoting the 💩 out of this new, shiny feature.
But as an engineering team, our work is not over yet. That is, unless you intend to quit tomorrow and let your successors deal with this code one beautiful day when it breaks. I used a few shortcuts (pun intended) in this article to keep it simple. Continue reading for further discussion.
- Bundle Optimisation
- Cross-Browser Support
- DevTools
- Edge Cases
- Pattern Matching
- Shortcut Selection
- State Management
- Support Multiple Shortcuts
- Testing
Bundle Optimisation.
There are 2 things to consider. Instead of using multiple string literals with the same value, we could replace them with a single variable. This variable could then get minified during our build process to shave some bits off the final package.
The second point would be figuring out whether we need to ship this functionality at all? After all, these are called keyboard shortcuts for a reason. You might not want to serve this code to your mobile users without keyboard. However, as of March 2018, there is no reliable API to detect a presence of a keyboard.
Cross-Browser Support.
This guide uses a modern set of JavaScript features. If you support older browsers, you would need to ensure you transpile your code and use older browser APIs for event detection.
DevTools.
For your internal debugging tools, you might want to see which shortcuts are currently registered if something goes wrong. This is very simple, you can just dump the contents of registeredShortcuts
.
Edge Cases.
Let's say one day we add an input field to our application allowing users to search layers. Now if they want to search for the "background" layer, they can start typing b, but oh là là , the brush tool gets selected. That's not the worst that could happen, we could be toggling some window or dialog, but it illustrates the issue.
For this kind of bugs, you will want to check the event.target
to ensure it is not a writeable DOM node.
Pattern Matching.
Yes, our shortcuts work. But try pressing Ctrl/⌘ + B – the brush tool still gets selected! This is clearly not what we want. So instead of a simple check for event.key
, we would need to add a precise pattern matching function.
Shortcut Selection.
Sasha Maximova wrote a good article on selecting shortcuts for your application so I won't regurgitate her points. Personally, I prefer to use one-key shortcuts and Shift for two-key combinations as it dramatically decreases chances of clashing with any browser/OS commands. Exception are formatting functions like bold where there is no reason to use anything but Ctrl/⌘ + B since your users will be used to that.
State Management.
I have restrained from throwing a whole state management library like Redux into our tutorial to keep it short and simple, but you would probably want to iterate on managing a state in your app. We have inadvertently created a few global variables that would be easily contained with tools like Redux.
Support Multiple Shortcuts.
Right now, our function accepts only a single string parameter, but there might be a case where you want to register multiple shortcuts at once. For example, if you prefer to declare all shortcuts on top of your view.
Testing.
Do not ship code without tests. It will be much easier to develop on top of the existing functionality with tests as well as catching potential bugs. If you use an SPA framework (I know you do), you will also want to document your components so it's easy to see which arguments they support.
Conclusion.
Adding keyboard shortcuts to your application is a quick win that can improve your user experience without costing you an arm and a leg. Carefully select the most used features and delight your users by enabling them to speed up their workflow.
Thank you for reading.
Thank you to friends at Scrimba for reading the draft of this article. Visit them for the easiest way to learn code. This article also appeared in Hacker Noon.