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.

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 over window when adding event listeners.
document.addEventListener('keyup', function (event) { });

Three points I want to make here.

  1. Prefer the use of document over window when adding event listeners. They’re consequentially the same, but document is closer to DOM elements than window is. So, adding listeners on document prevents the window from receiving them, which keeps the window clean and uninvolved with any keypress listeners. (source)
  2. 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 use keydown, probably (I say probably because there are cases where you do want to listen for keydown 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 use keypress, 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.

doWhateverYouWantNowThatYourKeyWasHit();

Put It All Together

document.addEventListener('keyup', function (event) {
if (event.defaultPrevented) {
return;
}

var key = event.key || event.keyCode;

if (key === 'Escape' || key === 'Esc' || key === 27) {
doWhateverYouWantNowThatYourKeyWasHit();
}
});

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.