Keyboard events

  1. Test stand {#test-stand}
  2. Key event properties
  3. Processing the character: keypress
  4. Cancelling user input
    1. Demo: char-uppercasing input
  5. Working with scan-codes: keydown/keyup
    1. Scan codes VS char codes
    2. Special actions
  6. Tasks and examples

Keyboard events is one of wilder parts of frontend development. There are inconsistencies and cross-browser problems.

But still there are recipes which help to cope with ordinary situations easily.

There are following keyboard events:

keydown
A key is pressed down.
keypress
A character key is pressed.
keyup
A key is released.

There is a fundamental difference between keypress and keydown.

  • Keydown triggers on any key press and gives scan-code.
  • Keypress triggers after keydown and gives char-code, but it is guaranteed for character keys only.

Test stand {#test-stand}

To better understand keyboards event, we’ll use the test stand.

Prevent default:      
Ignore:      
Focus on the input below and press.

Log:

document.getElementById('kinput').onkeydown = khandle
document.getElementById('kinput').onkeyup = khandle
document.getElementById('kinput').onkeypress = khandle

function khandle(e) {
  e = e || event
  if (document.forms.keyform[e.type + 'Ignore'].checked) return
   
  var evt = e.type
  while (evt.length < 10) evt += ' '
  showmesg(evt + 
    ' keyCode=' + e.keyCode + 
    ' which=' + e.which + 
    ' charCode=' + e.charCode +
    ' char=' + String.fromCharCode(e.keyCode || e.charCode) +
    (e.shiftKey ? ' +shift' : '') +
    (e.ctrlKey ? ' +ctrl' : '') +
    (e.altKey ? ' +alt' : '') +
    (e.metaKey ? ' +meta' : ''), 'key'
  )
  
  if (document.forms.keyform[e.type + 'Stop'].checked) {
    e.preventDefault ? e.preventDefault() : (e.returnValue = false)
  }
}

In the test stand, char = String.fromCharCode(e.keyCode || e.charCode).
Unknown properties and methods are explained in details below.

  • Try pressing down a character key, like ‘S’, ‘1’ or ‘,’.
    It triggers keydown and then keypress. When the key is released, the keyup occurs.
  • Try pressing a special key like ‘Shift’, ‘Delete’ or ‘Arrow Up’.
    It triggers keydown and then keyup on the time of releasing.

Firefox and Opera trigger keypress for most special keys.
IE also triggers keypress for Esc.

It is possible that a special key results in keypress, but generally browsers don’t trigger it (and they shouldn’t).

The rule:

  • keydown/keyup are for any keys.
  • keypress is for characters.

Key event properties

There was a heck crazy zoo in keyboard events few years ago. Now we live iin happy time. Most terrible bugs and inconsistencies are fixed in recent browsers. IE is also pleasant to deal with. There are just several tricks to use.

Keyboard event have the following specific properties:

keyCode
The scan-code of the key. For example, if an “a” key is pressed, the character can be “a” or “A” (or a character from another language), but the keyCode is same. It depends on key only, not on the resulting character.

You can always check the code by clicking on the test stand… There are two main tables: Mozilla and IE. They are almost equal, but differ in few keys: ';', '=' and '-'.

You can read a great article from John Walter: JavaScript Madness: Keyboard Events. It contains both events information and code tables.

charCode
The character code, ASCII.
which
A non-standard property, the hybrid of charCode and keyCode, with the sole purpose to confuse a developer.
But in the zoo of key events it also plays a good role. Particulary, it helps to get the character. That’s described further in the section.
shiftKey, ctrlKey, altKey, metaKey
The properties are boolean and reflect the state of corresponding key: Shift, Ctrl, Alt or Command(Mac only).

Processing the character: keypress

`keypress` is deprecated, but still the way to go

The latest DOM 3 Events specification deprecates keypress and replaces it by textInput.

But as of now, textInput event is supported in Safari/Chrome only, and the support is incomplete, so this is a far future.
The keypress is now.

The only event which reliably provides the character is keypress.

In all browsers except IE, the charCode property is defined for keypress and contains the character code. Opera follows this principle, but bugs on special keys. It triggers keypress without charCode on some of them, e.g “backspace”.

Internet Explorer has it’s own way. In case of keypress event it doesn’t set the charCode, but puts the character code in keyCode instead of scan-code.

So here’s the function to get all a symbol from keypress event:

// event.type must be keypress
function getChar(event) {
  if (event.which == null) {
    return String.fromCharCode(event.keyCode) // IE
  } else if (event.which!=0 && event.charCode!=0) {
    return String.fromCharCode(event.which)   // the rest
  } else {
    return null // special key
  }
}

Note the last case. Special keys have no symbols. Applying String.fromCharCode to special keys gives weird results.

We filter them with event.which!=0 && event.charCode!=0 check. It guarantees that the key is not special even in older browsers and Opera.

The wrong `getChar`

You can also find the following function in the net:

function getChar(event) {
  return String.fromCharCode(event.keyCode || event.charCode) 
}

It works wrong for many special keys for the reason described above. For example, it returns character ‘&’ when ‘Arrow Up’ is pressed.

Cancelling user input

A non-special key usually results in a character. This can be prevented.

For all browsers except Opera, two events can be used to cancel a key input: keydown and keypress. But Opera is more picky. It will cancel character only if preventDefault comes from keypress.

Try to type something in the input below:
<input *!*onkeydown="return false"*/!* type="text" size="30">
<input *!*onkeypress="return false"*/!* type="text" size="30">

Try to type something in the input below:

In the example above, there will be no characters in both inputs for all browsers with exception of Opera which ignores preventDefault on keydown and hence will show keys on the first input.

In IE and Safari/Chrome preventing default action on keydown cancels keypressed event too. Try that on the test stand: prevent keydown and type something. There should be no keypressed in IE and Safari/Chrome.

Demo: char-uppercasing input

The following input uppercases all characters:

<input id='my' type="text">
<script>
document.getElementById('my').onkeypress = function(event) {
  var char = getChar(event || window.event)
  
  if (!char) return // special key
  
  this.value = char.toUpperCase()
  
  return false
}
</script>

Characters which you type here will be upper-cased:

In the example above, the default action is prevented by return false, our own value is added instead.

There is a problem with this widget. If you move the cursor in the middle of input and type something - it appends to the end. So, the uppercasing input is not so simple, but still doable, because there exists a way to get caret position.

Write an input which accepts only digits. There is a demo below.

Open solution
Solution

We need characters here, so the event is keypress.

The algorithm is to take the char and see if it is numeric. Cancel default action if it’s not.

The only minor pitfall is to accept special chars as well as numbers. Otherwise it will become impossible to use arrow keys and delete in browsers which generate keypress on them. Namely, Firefox.

So, here’s the solution:

input.onkeypress = function(e) {
  e = e || event
  var chr = getChar(e)
  
  if (!isNumeric(chr) && chr !== null) {
    return false
  }
}

Helper function getChar gets the character, isNumeric checks for number.

The full solution code is here: tutorial/browser/events/numeric-input/index.html.

Working with scan-codes: keydown/keyup

Sometimes, we need to know only a key, not the character. For example, special like arrows, page up, page down, enter, escape - there are no characters at all.

Most browsers do not generate keypress for such keys. Instead, for special keys the keydown/keyup should be used.

The good news is that modern browsers and (even older) IE agree on keycodes for almost all special keys (with the exception of branded keys like IE start button).

Another example is hotkeys. When we implement a custom hotkey with JavaScript, it should work same no matter of case and current language. We don’t want a character. We just want a scan code.

Scan codes VS char codes

As we know, the char code is a unicode character code. It is given only in the keypress event.

A scan-code is given on keydown/keyup.

For all alphanumeric and most special keys, the scan code generally equals a character code. In case of a letter, the scan code equals an uppercased english letter char code.

For example, you want to track “Ctrl-S” hotkey. The checking code for keydown event would be:

e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)

And it doesn’t matter if user the resulting char is “s” or “S” or another language letter.

For alphanumeric keys, the scan code equals the character code of the uppercased english letter/digit.

The scan code do not equal the char code for most punctuation characters including brackets and arithmetic symbols.

For example, a “-” key has keyCode=109 in Firefox, keyCode=189 in IE, but it’s charCode=45. Obviously no match.

For all keys except ';', '=' and '-' different browsers use same key code.

Try that on the test stand above. Type ‘-’ and watch keydown keyCodekeypress charCode.

Special actions

Some special actions can be prevented. If the backspace is pressed, but the keydown returns false, the character will not be deleted.

But of course certain actions can’t be cancelled, especially OS-level ones. Alt+F4 closes browser window in most operating systems, no matter what you do in JavaScript.

Tasks and examples

Click on the mouse below. Then press arrow keys, it will move.

The task is a prototype of the real keyboard navigation on an interface.

The source document and mousie await you here: tutorial/browser/events/mousie-src/index.html.

The getOffset for absolute coords is attached too, if you need it.

Open hint 1
Hint 1

On click focus on the mouse to accept keys on it and bind keydown handler.

Open hint 2
Hint 2

Position the mouse absolutely. Use getOffset from article Metrics to get current position.
Learn exact values of keyCodes for arrow keys by pressing them on the test stand in the article or in browser manual.

Open hint 3
Hint 3

Prevent default behavior or the page will scroll too.

Open solution
Solution

The algorithm

There are few steps which comprise the solution:

How to track when a mousie is in clicked state and when it’s not?
An obvious solution here is focus. Key events will trigger on the focused element and bubble up.

To make DIV focusable, we should add tabindex:

<div style="width:41px;height:48px;background:url(mousie.gif)" id="mousie" tabindex="0"></div>

How to track keys?
We need to track arrow keys. So there are two events in our disposal: keydown and keyup. We choose keydown, because it allows to cancel the default action, which is page scrolling.
How to move the mousie?
Like any other element: position:absolute, left and top change depending on the key.

Changing position on click.

In the beginning, the mousie has static position. First, we need change it to absolute on click or on focus which gives better accessibility, because the focus can be given by keyboard tab too.

An absolutely positioned mousie will stick to the left-upper corner of the BODY. To keep mousie at same place, we need to set left/top to it’s current coordinates:

document.getElementById('mousie').onfocus = function() {
  this.style.position = 'absolute'
  var offset = getOffset(this)  
  this.style.left = offset.left + 'px'
  this.style.top = offset.top + 'px'
}

The function getOffset is described in Metrics.

Also, the pitfall is that if the mousie is surronded by other elements, they will shift. An absolutely positioned element jumps out of the flow:

<div style="color:green">Before</div>

<div onclick="this.style.position = 'absolute'" style="cursor:pointer"> 
  Click me 
</div>

<div style="color:red">After</div>

To fix it, we need a placeholder or a wrapper, like this:

<div style="width:41px; height:48px">
  <div style="width:41px;height:48px;background:url(mousie.gif)" id="mousie" tabindex="0"></div>

</div>

The outer DIV occupies the space no matter if the contents exists (in flow) or not.

Moving the beast

The codes for arrows are 37-38-39-40 (left-top-right-bottom).

document.getElementById('mousie').onkeydown = function(e) {
  e = e || event
  switch(e.keyCode) {
  case 37: // left
    this.style.left = parseInt(this.style.left)-this.offsetWidth+'px'
    return false
  case 38: // up
    this.style.top = parseInt(this.style.top)-this.offsetHeight+'px'
    return false
  case 39: // right
    this.style.left = parseInt(this.style.left)+this.offsetWidth+'px'
    return false
  case 40: // down
    this.style.top = parseInt(this.style.top)+this.offsetHeight+'px'
    return false  
  }
}

Note that the default action of arrows is to scroll the page. So we have to return false to prevent it.

There is no need to remove handlers on blur, because the browser will stop triggering keydown. So when a user blurs the mousie, it stops reacting on keys.

The final solution is tutorial/browser/events/mousie/index.html.

Create an input that warns user if the Caps Lock is on. Releasing Caps Lock removes the warning. This may help to prevent errors when entering password.

The source document: tutorial/browser/events/capslock-src/index.html.

Open hint 1
Hint 1

Detect Caps Lock the following way:
if a key pressed without shift and it’s in the upper case - means caps is on.

Open solution
Solution

How to track Caps Lock?

Unfortunately, there is no direct access to the state.

But we could use the events:

  1. Check keypress events. An uppercased char without shift or a lowercased char with shift means that Caps Lock is on.
  2. Check keydown for Caps Lock key. It has keycode 20.

For reliability both keydown and keypress events should be tracked on page-level.

On page load, before anything was printed, we know nothing about Caps Lock, so the state is null:

var capsLockEnabled = null

When a key is pressed, we can try to check if character case and shift do not match:

document.onkeypress = function(e) {
  e = e || event 

  var chr = getChar(e)
  if (!chr) return // special key

  if (chr.toLowerCase() == chr.toUpperCase()) {
    // caseless symbol, like whitespace 
    // can't use it to detect Caps Lock
    return
  }

  capsLockEnabled = (chr.toLowerCase() == chr && e.shiftKey) || (chr.toUpperCase() == chr && !e.shiftKey)
}

When a user presses Caps Lock, we should change current Caps Lock state. But we can do it only if we know it.

For example, when a user enteres page, we don’t know if Caps Lock is on. Then a keydown for Caps Lock detected. But we still don’t know what the new state is - did he disable Caps Lock or enable it.

document.onkeydown = function(e) {
  e = e || event
  
  if (e.keyCode == 20 && capsLockEnabled !== null) {
    capsLockEnabled = !capsLockEnabled
  }
}

Now, the input. The task is to show a warning about Caps Lock On to protect the user from password errors.

  1. First, the user focuses on it. We should show Caps Lock warning if we know it’s enabled.
  2. The user starts to type. Every keypress bubbles up to document.keypress handler which updates capsLockEnabled.

    We can’t use input.onkeypress to indicate the state to the user, because it will work before document.onkeypress (cause of bubbling) and hence before we know the Caps Lock state.

    There are many ways to solve this problem. We’ll stick to simplest and assign caps lock indication handler to input.onkeyup. It always happens after keypress.

  3. At last, user blurs the input. The Caps Lock warning may happen to be on, but it is not needed any more if the input is blurred. So we need to hide it.

The input checking code:

<input type="text" onkeyup="checkCapsWarning(event)" onfocus="checkCapsWarning(event)" onblur="removeCapsWarning()"/>

<div style="display:none;color:red" id="caps">Warning: Caps Lock is on!</div>

<script>
function checkCapsWarning() {
  document.getElementById('caps').style.display = capsLockEnabled ? 'block' : 'none'
}

function removeCapsWarning() {
  document.getElementById('caps').style.display = 'none'
}
</script>

The full code for the solution is here: tutorial/browser/events/capslock/index.html.

Create a DIV which becomes an editable TEXTAREA when Ctrl-E is pressed.
When in edit mode, the changes are saved to DIV on Ctrl-S and junked on Esc. After it, the TEXTAREA becomes the DIV again.

The contents is saved as HTML, tags should work.

Please look at how it should look: tutorial/browser/events/hotfield/index.html.

The source code is here: tutorial/browser/events/hotfield-src/index.html.

Open solution
Solution

As you notice in the source code, #view is the DIV for the result and #area is the editable textarea.

The look

First, the look. Because we transform a DIV into TEXTAREA and back, we make them look almost same:

#view, #area {
  height:150px;
  width:400px;
  font-family:arial;
}

The textarea should be emphased somehow. A possible way is to add aborder. But if I set the border, this will change the box, enlarge it and shift the text a little bit.

To make #area size same as #view we use padding:

#view {  
  /* padding + border = 3px */
  padding: 2px; 
  border:1px solid black; 
}

The #area

#area {
  border: 3px groove blue;  
  padding: 0px;

  display:none;
}

It is initially hidden. Also, the following piece hides extra border around focused textarea which appears in Chrome/Safari:

#area:focus { 
  outline: none; /* remove focus border in Safari */
}

Tracking codes

To track keys, we need their scan codes, not characters. That’s important, because such hotkey will work in all languages and all cases. So, keydown is a reasonable choice:

document.onkeydown = function(e) {
  e = e || event 
  if (e.keyCode == 27) { // escape
    cancel()
    return false
  }

  if ((e.ctrlKey && e.keyCode == 'E'.charCodeAt(0)) && !area.offsetHeight) {
    edit()
    return false
  }

  if ((e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) && area.offsetHeight) {
    save()
    return false
  }
}

In the example above, offsetHeight is checked to see if the element is visible. It is a very reliable way on all elements except for TR tag (works with tweaks).

Unlike simple display=='none' check it works with element hidden by styles (as we have here) and also for elements with hidden parents.

Editing

The following functions switch between modes. HTML is allowed, so the direct transform from/to TEXTAREA is possible.

function edit() {
    view.style.display = 'none'
    area.value = view.innerHTML
    area.style.display = 'block'
    area.focus()
}

function save() {
    area.style.display = 'none'
    view.innerHTML = area.value
    view.style.display = 'block'
}

function cancel() {
    area.style.display = 'none'
    view.style.display = 'block'
}

The full solution is here: tutorial/browser/events/hotfield/index.html.
To test it, focus in the right iframe please.

See also:

Tutorial

Donate

Donate to this project