Step 4 of 7
Input controls
Direct answer: Record input events as state, then read that state during the game loop so movement stays predictable.
What you need first
The game loop lesson. You should already be able to move a rectangle by changing x and y each frame.
After this lesson
You can steer a player with keys or on-screen buttons, stop the page from scrolling, and block an illegal Snake reverse.
Events are not movement
A keydown event fires when a key is pressed. It does not know how fast your game is updating. If you move the player inside the event handler, held keys feel uneven and the player can move while the game is paused.
The reliable pattern is:
- On
keydown/keyup, store which controls are held. - In
update(), read that stored state. - Only then change position.
const keys = new Set();
addEventListener('keydown', (event) => {
keys.add(event.key);
if (event.key.startsWith('Arrow')) event.preventDefault();
});
addEventListener('keyup', (event) => keys.delete(event.key));
function update() {
if (keys.has('ArrowLeft') || keys.has('a')) player.x -= player.speed;
if (keys.has('ArrowRight') || keys.has('d')) player.x += player.speed;
if (keys.has('ArrowUp') || keys.has('w')) player.y -= player.speed;
if (keys.has('ArrowDown') || keys.has('s')) player.y += player.speed;
}
preventDefault() stops arrow keys from scrolling the page. That matters as soon as the canvas sits on a long lesson page like this one.
When to use it
Use keyboard events for desktop controls; add visible touch controls when the game must work on phones. Continuous movement (a paddle, a ship) should read held keys every frame. Grid games such as Snake should store a direction and apply it on the next tick.
Try it and check it
Move the green square with arrows, WASD, or the buttons. Then hold an arrow key: the square should keep moving until you release it. If it only jumps once, the update is still inside the event handler.
Held: none
Snake needs a queued direction, not a held key
Our playable Snake remake does not slide while you hold an arrow. It stores the next legal heading and applies it on the next grid step. It also refuses an immediate reverse into the snake’s neck:
function opposite(a, b) {
return (a === 'left' && b === 'right') || (a === 'right' && b === 'left')
|| (a === 'up' && b === 'down') || (a === 'down' && b === 'up');
}
function queueDir(next) {
if (opposite(next, currentDir)) return;
queuedDir = next;
}
The shipped game goes one step further: it buffers up to two turns so a fast left-then-up still registers. The mini project later in this path uses the simpler one-slot queue. That is enough for a first Snake.
Phones need a visible control
There is no hover and no reliable arrow keyboard on a phone. Give the player something to tap. The demo buttons above write into the same state object as the keyboard. A swipe on a canvas works the same way: measure touchend - touchstart, pick the larger axis, then call queueDir().
Common mistake
Moving the player only inside a key event, which makes continuous movement difficult to control. A second common error is forgetting keyup, so the player keeps sliding after the key is released.
Real game connection
Try the controls in Snake. The remake uses keyboard, on-screen buttons, swipe, and Space to pause. After this lesson you only need the first three ideas. Pause belongs with game state.
Compatibility: Test in a current Chrome, Firefox, Safari, or Edge browser. Canvas and standard input work broadly in current browsers. Touch events fire on phones; desktop Safari still needs the keyboard or the on-screen buttons.
Source and update: Reviewed against MDN Web Docs for KeyboardEvent. Updated 2026-08-13. This page does not claim performance results beyond the local example check.
Next actions: detect collisions · build the Snake project · play Snake