WASD Controls on the Web: Don’t use KeyboardEvent.key but use KeyboardEvent.code

A while ago I was talking to the author of the aforementioned Jamir that the WASD controls they implemented weren’t that practical for me due to the AZERTY keyboard layout which I use.

Thankfully, the fix is pretty small and easy to roll out …

~

WASD vs. AZERTY

To control the position of characters in games, the gaming industry has settled on using the WASD keys to do so. These keys are laid out similar fashion to the arrow keys, can be controlled with the left hand, and keep your right hand free to control the mouse to turn and look around.

On QWERTY, using WASD works fine, as these keys are placed in line with a rather natural position of your left hand’s fingers:

But on AZERTY — the default keyboard layout in Belgium — it would look like this:

To not have to flex our fingers in some very weird ways, us Belgians have adopted the use of ZQSD, which maps to the same physical keys as WASD on QWERTY.

In most games this difference in keyboard layout is automatically handled, and pressing ZQSD works fine for me and my fellow AZERTY users. In Jamir, however, that’s not the case, and we’re required to press the key that has the label W in order to move forward.

~

The problem with KeyboardEvent.key

The culprit is that Jamir’s code is basing itself on KeyboardEvent.key to determine which key was pressed. Since KeyboardEvent.key represents the label that appears on the key, that will map to different physical positions per keyboard layout. You can see this in the images above: the W key has moved between QWERTY and AZERTY

Pressing the key with the label w on your keyboard — or any keyboard — will always show a value of w for KeyboardEvent.key, independent that key’s physical location on the keyboard.

To play nice with AZERTY, Jamir would have to maintain separate list of KeyboardEvent.key values to respond to ZQSD. Same for DVORAK, Maltron, QWERTZ, Colemak, JCUKEN, or any other keyboard layout …

  • User has QWERTY?
    → Listen for KeyboardEvent.key W to move forward.
  • User has AZERTY?
    → Listen for KeyboardEvent.key Z to move forward.
  • User has …?
    → Listen for KeyboardEvent.key ??? to move forward.

Seems like a tedious job to do and maintain 🫤

~

Keyboard Layout-Independent WASD

Thankfully there’s an easier solution than keeping track of multiple keyboard mappings: instead of evaluating KeyboardEvent.key, evaluate KeyboardEvent.code, as that represents the physical key that’s being pressed.

See the Pen
Keyboard Event Debugger
by Bramus (@bramus)
on CodePen.

Testing with the CodePen above, using several keyboard layouts, you can see that the value for KeyboardEvent.code remains the same when pressing the same physical key:

  • QWERTY keyboard, pressing W:

    {"keyCode":87, "key":"w", "which":87, "code":"KeyW"}
  • AZERTY keyboard, pressing Z:

    {"keyCode":90, "key":"z", "which":90, "code":"KeyW"}

If you have your code check the value of KeyboardEvent.code, the WASD controls will automatically adapt themselves to become ZQSD on AZERTY, ,AOE on DVORAK, etc. — yay! 🎉

  • User has QWERTY?
    → Listen for KeyboardEvent.code KeyW to move forward.
  • User has AZERTY?
    → Listen for KeyboardEvent.code KeyW to move forward.
  • User has any keyboard layout?
    → Listen for KeyboardEvent.code KeyW to move forward.

☝️ The thing that has to click here is that there’s a difference between the physical position of a key (exposed through KeyboardEvent.code) and the label on the key (exposed through KeyboardEvent.key). E.g. Z on AZERTY and W on QWERTY are the same physical key ("KeyW"), but they have a different label (z or w, depending on the layout).

~

Press W/Z/, to move forward

With KeyboardEvent.code in place, our controls will now play nice with all keyboard layouts, but it can now become a bit difficult to tell the user which key they need to press:

  • Using QWERTY?
    → Press W to move forward.
  • Using AZERTY?
    → Press Z to move forward.
  • Using DVORAK?
    → Press , to move forward.

👨‍🏫 Again, note that these all map to the same physical key ("KeyW") being pressed.

Thankfully there’s the experimental Keyboard.getLayoutMap() that can help us out here. Using it, we can translate a KeyboardEvent.code to it’s accompanying label:

const keyboardLayoutMap = await navigator.keyboard.getLayoutMap();
const forwardKey = keyboardLayoutMap.get('KeyW');
console.log(forwardKey);

The code above will return w on QWERTY, z on AZERTY.

👨‍🔬 Keyboard.getLayoutMap() is only supported in Chromium based browsers at the time of writing.

~

When not to use KeyboardEvent.code

Note that it’s not required everywhere to use KeyboardEvent.code. If you have an undo feature in your app that’s triggered by pressing the z key, then you do need to listen to KeyboardEvent.key.

It’s only in situations where the physical keys matter — such as WASD controls in games, a piano, etc. — that you need to resort to KeyboardEvent.code.

~

Want more?

If you want more: in the latest episode of HTTP 203, Jake and Ada tap into keyboard events and this use-case. Besides covering non-QWERTY keyboard layouts, they also cover virtual keyboards, compositions, swipey keyboards, etc.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.