Checking for Keyboard Events in JavaScript with Cross-Browser Support
How to use KeyboardEvent.key
to check which key was pressed with cross-browser support including IE 11 and older versions of Safari and Opera.
One of the biggest challenges of web application development compared to embedded application dev is that our application needs to run in a variety of environments or browsers. Providing the greatest coverage of support across varieties and versions of browsers is the perhaps under-appreciated discipline of cross-browser support. In this article I’ll be covering the current best-practice for listening for keyboard events with the greatest reasonable cross-browser support. I’ll be using vanilla JavaScript ES5 with additional examples in ES2015+.
What We Used To Do — And Still Do
For years and years and years, KeyboardEvent.keyCode
was the de facto solution for identifying which key was pressed when using vanilla JavaScript or jQuery. Collectively StackOverflow has over two thousand up-votes for answers which advocate using keyCode
ranging in date from 2009 to 2017. In fact, keyCode
is supported in all major browsers, including IE6. The thing is, however, that keyCode
is now deprecated (removed) from the ECMAScript KeyboardEvent
specification.
KeyCode
was deprecated because in practice it was “inconsistent across platforms and even the same implementation on different operating systems or using different localizations.” The new recommendation is to use key
or code
. However browsers currently still have better support for keyCode
, so inertia has stopped the transition of JavaScript developers towards following the new spec. Unless more developers start to actually follow the new specification, browsers (I’m looking at you, MS Edge) will continue to deprioritize those implementations.
How to Follow the Spec and Support All Browsers
Graceful degradation is the practice of building your web functionality so that it provides a certain level of user experience in more modern browsers, but it will also degrade gracefully to a lower level of user in experience in older browsers.
When a specification has moved past browser implementation (again, looking at you MS Edge) then there needs to be a clear path for upgrade which maintains browser support for low-end users while actually using the right features. However there’s a lack of leadership in this effort when it comes to practice. Specification creators haven’t strong taken the lead on this because the implementation is not their responsibility. Since there’s not a clear directive on this matter, it’s worth self-organizing to establish best practices for developers. There are many people who would like to follow the specification but are met with the issue of losing support for older browsers by doing so. The solution then, is to give top billing to the specification as implemented and gracefully fall back to provide support for other browsers.
But wait — isn’t browser implementation more important than an abstract specification?
keyCode
is implemented in all browsers, including the new ones, so why not just use that?
Well, strawman, I’m glad you asked. Browser implementation of code specifications is not only driven by the specifications themselves, but by usage patterns of developers. If something is strongly needed before the specification is finalized, a browser might implement an early version of the spec (KeyboardEvent.key
in IE11 uses “Esc” rather than “Escape” because it was implemented before the specification was finalized).
Once their implementation was done, they felt as though the feature was available so there was no need to update it. Since there was some interface, other work is and was prioritized above updating this implementation. The issue also lies with the fact that implementing the spec before it was finalized led numerous people to use that. If Edge were to change this implementation, it would break the apps which are looking for the old implementation. And that is why it’s a bad idea to spend effort implementing a spec before it is finalized.
Use event.key
first with graceful degradation to event.keyCode
var key = event.key || event.keyCode;
This code initializes key from event.key
if that property has a value that is not undefined. If that property has an undefined value, we’ll look for keyCode
.keyCode
is present in almost all browsers, but is deprecated in the spec. I repeat: keyCode
is technically deprecated.
Other Fallback Options
The main competitor options I could find were KeyboardEvent.keyIdentifier
, KeyboardEvent.detail.key
, KeyboardEvent.which
, and KeyboardEvent.code
. The reason I don’t suggest these is that, besides code
, they are all deprecated and also lack the wide support of keyCode
. KeyboardEvent.which
is notable as the option which jQuery leaned into and which provided the widest support when using jQuery.
KeyboardEvent.code
is not deprecated, however it doesn’t serve as a fallback to key. It could be used instead of key, however it uses special values to indicate it’s looking for the actual physical key on a keyboard which was pressed, and not the value of the character on the key being press. KeyboardEvent.key
is more naturalistic to work than KeyboardEvent.code
because it represents the value of the character of the key pressed, while code represents the physical key on the keyboard which was pressed (good for international).
I saw one issue which fell back to keyIdentifier
to keyCode
, but there’s no tangible benefit to doing this. In the corresponding pull-request, they end up dropping keyIdentifier
and only using keyCode
as I have here.
Add the Event Listener
I wanted to include a complete demonstration of how to add an event listener using this method.
Prefer the use of
document
overwindow
when adding event listeners.
document.addEventListener('keyup', function (event) { });
Three points I want to make here.
- Prefer the use of
document
overwindow
when adding event listeners. They’re consequentially the same, butdocument
is closer to DOM elements thanwindow
is. So, adding listeners on document prevents the window from receiving them, which keeps the window clean and uninvolved with any keypress listeners. (source) - Use
keyup
as the trigger event for UX reasons. For most behaviors (not games), people typing expect that the key they have pressed won’t get “entered” until they release the button. That’s why you don’t want to usekeydown
, probably (I say probably because there are cases where you do want to listen forkeydown
instead). They also expect that holding down a key won’t trigger the associated action unless they’re typing (If you are intercepting and reproducing regular text entry to this degree, my advice would be to re-evaluate whether you really need to do that. You probably don’t.). That’s why you don’t want to usekeypress
, probably.keypress
also can't be used with Alt, Shift, or Ctrl.
The Add Listener Function
if (event.defaultPrevented) {
return;
}
Inside the add listener function, let’s determine if we should be seeing this event at all. If another event handler has already captured this event and prevented its default behavior, we don’t want to do anything with it, probably. To be honest, I was surprised to see this recommended. I would have expected that calling preventDefault
on the event elsewhere would have prevented the event from reaching this function. I’m curious if anyone knows for sure what the behavior is and how necessary it is to include this. Since it doesn’t hurt anything, I’m including it anyway.
var key = event.key || event.keyCode;
We are gracefully degrading from event.key
to event.keycode
here.
if (key === 'Escape' || key === 'Esc' || key === 27) { }
This code listens for an Escape key keyup event. I am including it here because I want to highlight how to handle the fall back strategy effectively. Escape, as well as some other common keys such as the arrow keys, were given names in versions of IE and Edge that were implemented on an earlier spec. Thus, if our JavaScript runs in that browser, it would produce a false negative to check if the value was ‘Escape’ when it is ‘Esc.’ Since some (Microsoft) browsers have this earlier code string value, we need to check for it for our JavaScript to run as anticipated on those older browsers.
Finally, if user ended up falling back to event.keyCode
due to the browser they were using, we need to check for the actual keycode value. keyCode
uses integer values to represent keys. As fine as this sounds in theory, in practice different browsers used different implementations anyway. That was the reason the property was deprecated in the first place. If you are trying to use keyCode
with special characters, your code may not behave as expected on some older browsers. EDIT: keyCode
is now deprecated
doWhateverYouWantNowThatYourKeyWasHit();
Put It All Together
function keyListener(event) {
if (event.defaultPrevented) {
return;
}
var key = event.key || event.keyCode;
if (key === 'Escape' || key === 'Esc' || key === 27) {
doWhateverYouWantNowThatYourKeyWasHit();
}
}document.addEventListener('keyup', keyListener);
Thanks for reading. Please check out my other recent posts.
EDIT: Thanks to Lightone for pointing out the keyCode
value for escape should be 27, not 23.
Thanks to @ssejjembadan for pointing out that passing an anonymous function to an event listener has the potential to introduce memory leaks.