Fixing Common Browser Issues: Dealing With Key Events

Ben Lowery | August 11, 2010

Key handling is often an after-thought in most web applications, so today we're going to shed some light on how to properly handle key events in your application. We'll cover what the key events are (keydown, keypress, and keyup), what they tell us, and how to use them. Additionally, we'll cover some of the significant differences in when the major browsers fire them and how to work around the inconsistencies.

The Events

Your application can listen for key events through three events: keydown, keypress, and keyup. Let's look at what's available in the spec, and how the various browsers define each event.

The only description for these events comes from HTML 4, sec 18.2.3:   

onkeypress = script [CT]The onkeypress event occurs when a key is pressed and released over an element. This attribute may be used with most elements.

onkeydown = script [CT]The onkeydown event occurs when a key is pressed down over an element. This attribute may be used with most elements.

onkeyup = script [CT]The onkeyup event occurs when a key is released over an element. This attribute may be used with most elements.

Even as a high level overview, the descriptions given are pretty vague. There's no mention of whether keyup happens before or after keypress, how a held-down key is repeated, or what is reported back to the script handling the event. It's really no surprise that browser vendors vary. Looking forward, there is some hope for sanity in the DOM3 Events spec, but so far only WebKit has started implementing it. Let's take a peek at what we find with each major current vendor. This discussion will not be exhaustive, for a more complete contemplation of the issue, I highly recommend you read Jan Wolter's writeup on the current state of affairs. First, let's go over what the browsers do have in common.

Commonalities

  1. All of the major vendors support keydown, keypress, and keyup, firing in that order. When you push down the A key, a down->press->up sequence fires. 
  2. The keydown and keyup events fire for almost all keys, both alpha-numerics and special keys. These events  always fire, even if you prevent keydown's default action.
  3. The current state of the shift, control, alt and meta keys are available from the event object as boolean properties. If you were to hold down Shift and press A, the down, press, and up events for the A key would all have event.shiftKey set to true. 
  4. All browsers let you cancel the keypress event to prevent the character being typed from being sent to an input field. 
  5. Only DOM elements that can gain focus can fire key events. All key events will bubble, while only keydown and keypress are cancelable.

Sadly, that's about it. Browers differ enough to be annoying in how they report which key was pressed for keyDown and keyUp or what character would be inserted for a keyPress, on what preventing default behavior on keydown means, and on handling repeating due to held keys.

Happily, things do tend to fall into one of two camps, either the Internet Explorer camp or the Mozilla camp. WebKit (Safari & Chrome) mostly adopted IE's treatment, but some does expose part of the Mozilla model as well. Opera is mostly in the Mozilla camp these days, but is still different enough to warrant it's own treatment. Let's start with reporting.

Key and Character Reporting

IE takes the following approach: keydown and keyup represent physical key presses and expose the key as a "virtual key code," while keypress events represent character insertion and expose the character as a Unicode character code. All keys fire keydown and *keyup *events, including special keys like arrows, shift, control, as well as character keys. Only character keys fire keypress events. Microsoft takes advantage of this and reports the key or the character using the same keyCode property on the event object. For example, if you were to do the following (assuming theInput is a text box):

function printKeyCode(event) { 
  alert(event.keyCode); 
} 

theInput.attachEvent("onkeypress", printKeyCode); 
theInput.attachEvent("onkeydown", printKeyCode); 
theInput.attachEvent("onkeyup", printKeyCode);

and you hit Shift-9 to insert a left parenthesis, you will see the following sequence:

keyevent keyCode represents
down 16 SHIFT
down 57 9
press 40 (
up 57 9
up 16 SHIFT

 

57 is the virtual key code for the '9' key and 40 is the character code for '('. If you need it, you can turn the keypress keyCode back into a real character using String.fromCharCode().

The differentiation Microsoft makes becomes important when you realize that some virtual key codes overlap different Unicode code points. For example, as we just saw, left paren has a character code of 40. Well, the Down Arrow key has a virtual keyCode of 40. So, when you hit down arrow, you'll see this sequence:

keyevent keyCode represents
down 40 DOWN ARROW
up 40 DOWN ARROW

Consequently, if you want to know when special keys (or specific physical keys) are being pressed, you'll need to catch keydown. This is handy for things like menu bars and shortcut keys. If you want to know what characters are being inserted, you'll need to catch keypress. This is handy for as-you-type form validation. There is one wrinkle here; backspace does not fire keypress, so if you're doing character by character validation, you might need to catch keydown as well.

Firefox takes a slightly different approach. Like IE, just about every key fires keydown and keyup. However, just about every key fires keypress as well; everything except Shift, Control, Alt and Meta (Windows or Apple key) fires keypress. 

To differentiate character events from non-character events, keypress events, Mozilla uses a charCode property on the event object. If the charCode is zero, the event came from a non-character inserting key. If charCode is not zero and keyCode is, the keypress represents a character. Like IE, the charCode is a Unicode character code. Mozilla also fills out the which property, filling it with the keyCode for keydown and keyup and charCode for keypress. Consequently, which will be zero for non-character keys on keypress.

Repeating our previous example, if you hit Shift-9 for (, you'll see the following sequence:

keyevent keyCode charCode represents
down 16 0 SHIFT
down 57 0 9
press 0 40 (
up 57 0 9
up 16 0 SHIFT

For down arrow, you'll see the sequence:

keyevent keyCode charCode represents
down 40 0 DOWN
press 40 0 DOWN
up 40 0 DOWN

This is nice in a way, as you really only have to listen to one event to catch almost anything you'd care about, but there are some discrepancies with IE that make life interesting. First, Mozilla uses most of the same key codes as IE, but not all the same. Some symbols (semi-colon, equals and dash) have different keyCodes key keydown and keypress events, and Mozilla on Mac has an interesting bug where it sometimes drops keyCodes. For instance, hitting Shift-dash to get an underscore ends up with this sequence:

keyevent keyCode charCode represents
down 16 0 SHIFT
down 0 0 ???
press 0 95 _
up 0 0 ???
up 16 0 SHIFT

WebKit mostly adopted IE's tact in version 525, which ended up in a Safari update after Leopard shipped. They extend the model a bit by including Mozilla's charCode for keyPress event, and by filling out the W3C's which property, along with some new properties from the proposed DOM3 spec. Like IE, WebKit does not fire keypress for special keys like backspace and the arrow keys. The easiest way to deal with a modern WebKit is to treat it just like IE, even though it does expose which and charCode. The upcoming IE9 appears to now  follow the WebKit model, adding which and charCode

Last, Opera. Opera does it's own thing. Instead of using virtual key codes for the special keys, it reports the ASCII code for the character that would have been inserted if the key was pushed with no modifiers. Opera also reports using keyCode and which, but not charCode. This can get a little tricky for the down arrow and left paren case illustrated above, but Opera does have a solution. When a non-character key is pushed, which will be zero. When a character key is pushed, which will be equal to the keyCode. So, left paren looks like this:

keyevent keyCode which represents
down 16 16 SHIFT
down 57 57 9
press 40 40 (
up 57 57 9
up 16 16 SHIFT

and down arrow looks like this:

keyevent keyCode which represents
down 40 40 DOWN
press 40 0 DOWN
up 40 40 DOWN

Repeats

So far, we've only talked about what happens when you press and key and let it go. What happens when you hold one down? As you might expect, you get a repeating set of key events. Additionally, if you hold down more than one key at a time, you only get repeats for the last character pressed. However, what events you get varies. 

On IE and WebKit, when you hold down a key, you get repeating keydown AND keypress events for character keys and just repeating keydown events for non-character keys. For example, if I hold down 'a', I get:

keyevent keyCode represents
down 65 A
press 97 a

down 65 A
press 97 a
up 65 A

IE also adds a repeat: true property to subsequent keydown events, but not keypress events. WebKit does not include this property.

Firefox and Opera only repeat the keypress event. Again, for the held down 'a' on Firefox:

keyevent keyCode charCode represents
down 65 0 A
press 0 97 a

press 0 97 a
up 65   A

For non-character keys, like down arrow, we'd see this down, down, down, up on IE and WebKit and down, press, press, press, up on Firefox and Opera. Firefox and Opera never repeat keys that do not fire keypress (Shift, Control, Alt, Meta); thankfully IE and WebKit never repeat those keys either.

Again, your safest bet is to listen for keypress and catch the repeats that way, unless you need to catch repeating non-character keys on IE and Safari, in which case you'll have to listen keydown instead. 

Preventing Default Behavior and Repeating

As you probably know, the event model allows an event listener to prevent the default action that would have occurred as a result of the event from going forward. In the W3C model, supported by Mozilla and Opera (and IE9), you call event.preventDefault(). On older editions of IE, you set event.returnValue = false; In the case of clicks on a hyperlink, this would prevent the link from being followed. For keys, the matter gets a little more complicated.

All of the browsers support preventDefault in same way for keypress. Preventing the default action will stop the character from being sent to the corresponding DOM element. This can be a good way to prevent keystrokes from ending up in something like a credit card number text box. Also, preventing on keyup never does anything, as keyup is never cancelable. That's the good news.

The bad news is preventing keyDown has strange and disparate consequences. On IE and WebKit, it will prevent a keypress event from ever being fired; not so on Firefox or Opera. Additionally, on Firefox and Opera, preventing keyDown will stop the first character from being sent to a DOM element, but it will not stop repeats! So, if you want to prevent a keystroke from reaching a DOM element, your best bet is to intercept it in a keypress handler, unless it's a key you can't listen for in keypress, like arrow keys on IE or WebKit.

That's basically it for behavior. It's crazy, but mostly manageable, especially if you're trying to do something fairly constrained, like catch arrow keys or prevent certain characters from being entered into a text box with the keyboard. Next, let's take a peek how some of the major Javascript toolkits try to rationalize this behavior.

Frameworks

Given all the information presented so far, you could roll you own key handling framework, but why bother when other's have done it already? Let's quickly cover how to accomplish a simple task, changing the value in a spinner in response to arrow presses with repeats in jQuery, Dojo, Google Closure Library, and MooTools.  We'll assume we have to following bit of HTML available:

<input type="text" id="anInput" value="0"> 
<script type="text/javascript"> 

var _anInput = document.getElementById("anInput"); 

function increase() { 
  _anInput.value = parseInt(_anInput.value,10) + 1; 
} 

function decrease() { 
  _anInput.value = parseInt(_anInput.value,10) - 1; 
} 
</script>

jQuery

var UP_ARROW = 38, 
    DOWN_ARROW = 40; 

function dispatchArrow(keyCode) { 
  switch(keyCode) { 
    case UP_ARROW: 
      increase(); break; 
    case DOWN_ARROW: 
      decrease(); break;   
    } 
} 

// yes, i know, browser detection is so 1999. 
// sadly, no other way to work around these quirks 

// this fires and repeats on webkit and ie 
if($.browser.webkit || $.browser.msie) { 
  $("#anInput").keydown(function(evt) { 
    dispatchArrow(evt.keyCode); 
  }); 
} 

// this fires and repeats on mozilla and opera 
if($.browser.mozilla || $.browser.opera) { 
  $("#anInput").keypress(function(evt) { 
     // have to check 'which' on the original event 
     // because jQuery overwrites it on Opera 
     if(evt.which === 0 || evt.originalEvent.which === 0) { 
       dispatchArrow(evt.keyCode); 
     } 
  }); 
}

jQuery takes a pretty simple approach to key handling; they try to rationalize the "which" property of the event object for all key events. This is done by setting which to either charCode or keyCode, prefering charCode if it exists. This gives you a which to inspect at on IE and Opera, but can get a little confusing on Opera. As stated earlier, Opera differentiates arrow keypresses from some characters, like left parenthesis, by setting which to zero ; jQuery overwrites the 0 with the keyCode and makes it look just like a character press. 

If you're on jQuery, you're best off catching non-printing keys with keydown or keyup, not keypress. You can check out their docs on keydown, keypress and keyup for more information.

Dojo

dojo.query("#anInput").onkeypress(function(evt) { 
  switch(evt.charOrCode) { 
    case dojo.keys.UP_ARROW: 
      increase(); 
      break; 
    case dojo.keys.DOWN_ARROW: 
      decrease(); 
      break;   
  } 
});

Dojo takes a more aggressive approach and tries to rationalize key events against the Mozilla model. To make this work, Dojo listens for keydown events and synthesizes keypress events when you subscribe to keypress. So, you can listen for keypress on an element and get notified of arrow keys, even on IE and WebKit. Dojo also adds some extra properties to the event object for keypress events, keyChar and charOrCode. keyChar is the actual character, while charOrCode is either the actual character, or the key code associated with the key if the key is unprintable. Dojo also exposes a normalized set of key codes, which you can use instead of hard coding numbers.

With Dojo, it's best to subscribe to keypress and ignore keydown and keyup. 

Google Closure Library

var anInput = document.getElementById("anInput"); 
var keyHandler = new goog.events.KeyHandler(anInput); 

goog.events.listen(keyHandler, "key", function(evt) { 

  var codes = goog.events.KeyCodes; 

  switch(evt.keyCode) { 
    case codes.UP: 
      increase(); break; 
    case codes.DOWN: 
      decrease(); break;   
  } 

});

Closure takes an entirely different approach. Instead of trying to augment or alter the native event object, Closure exposes a completely new key event abstraction, called a KeyHandler. KeyHandler fires a KeyEvent when it detects a key stroke and fires this event for all keys, printable or not. The key event has a keyCode and a charCode property, with charCode being filled out for key strokes that result in a printable character. Closure also exposes a set of normalized key codes, so you don't have to come up with your own cross-browser mapping.

MooTools

function dispatchArrow(key) { 
  switch(key) { 
    case "up": 
      increase(); break; 
    case "down": 
      decrease(); break;   
  } 
} 

// this fires and repeats on webkit and ie 
if($.browser.webkit || $.browser.msie) { 
  $("anInput").addEvent("keydown", function(evt) { 
    dispatchArrow(evt.key); 
  }); 
} 

// this fires and repeats on mozilla and opera 
if($.browser.mozilla || $.browser.opera) { 
  $("anInput").addEvent("keypress", function(evt) { 
     dispatchArrow(evt.key); 
  }); 
}

MooTools uses a custom event object which exposes a "key" property and a "code" property. "key" represents the key which was pressed, while "code" represents the character code for printable characters. "key" is a bit different, in that it exposes key codes as lowercase ascii characters, or a string like "enter", "up", "down", "left, "right", etc.   In addition, MooTools exposes the original event object on an event property of their custom event object, so you can always fall back on the charts above to figure out what's going on. 

Wrapping Up

Key events are very useful, but a bit quirky. Armed with a little knowledge, and some charts, you can easily start reacting to key events and add some great features to your application. I hope you found it useful.

 

About the Author

Ben has been spinning webs since Halloween of 1994 and has the gray hair to prove it. A long-time advocate for progressive enhancement through scripting, Ben is a committer on the Dojo project, and now spends his time teaching web pages to dance to a beat.

Find Ben on: