How to Code Snake in JavaScript (Browser Tutorial)

Snake is the second great clone-it-yourself game after Pong. A grid, a moving array of cells, a piece of food, and a game-over check. The whole thing fits in 80 lines of JavaScript and runs in any browser without a framework. This guide covers how to code Snake in JavaScript from scratch — the grid model, direction tracking, food spawning, and the self-collision check that ends the game.
Key takeaways
- Snake is grid-based — each cell is a fixed pixel size and the snake is an array of {x, y} cells.
- Direction is tracked as a single dx/dy vector that updates on key press; the snake moves one cell per tick.
- Food spawns randomly on an empty cell; eating it grows the snake and increases the score.
- Self-collision is checked by testing if the new head position overlaps any existing body cell.
- The game runs on a setInterval (or fixed-rate requestAnimationFrame) tick, typically 100-150ms per move.
The HTML setup
One canvas, one script. Keep the canvas small enough that the grid fits cleanly — 400×400 with a 20px cell size gives you a 20×20 grid.
<canvas id="game" width="400" height="400"></canvas>
That’s all the markup. Everything else happens in JavaScript.
Grid representation
The cell size sets the granularity of the game. A 20-pixel cell on a 400×400 canvas means a 20×20 grid (400 cells total). All positions are stored in grid coordinates, not pixels — you multiply by cell size only when drawing.
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const CELL = 20;
const COLS = canvas.width / CELL; // 20
const ROWS = canvas.height / CELL; // 20
Working in grid coordinates means movement is always one cell per tick — no fractional positions, no float rounding errors. The snake either moves a full cell or it doesn’t.
The snake as an array
The snake is an array of cell objects. The first element is the head; the rest is the tail. Each tick, you add a new head in the movement direction and remove the last tail cell — unless food was eaten, in which case you keep the tail (growing the snake by one).
let snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
];
The snake starts as three cells stretching left from the center. The tail end is where the snake started; the head moves first.
Direction tracking
Direction is a single delta: dx and dy. At any moment, exactly one is non-zero (the snake moves only in 4 directions, not diagonally).
let dir = { x: 1, y: 0 }; // start moving right
Input listeners change `dir` based on arrow keys — but only if the new direction isn’t directly opposite (you can’t reverse the snake into itself).
document.addEventListener('keydown', e => {
if (e.key === 'ArrowUp' && dir.y === 0) dir = { x: 0, y: -1 };
if (e.key === 'ArrowDown' && dir.y === 0) dir = { x: 0, y: 1 };
if (e.key === 'ArrowLeft' && dir.x === 0) dir = { x: -1, y: 0 };
if (e.key === 'ArrowRight' && dir.x === 0) dir = { x: 1, y: 0 };
});
The `dir.y === 0` check on Up/Down ensures the player can’t U-turn — if the snake is currently moving horizontally, only vertical inputs are accepted. Same for the horizontal directions.
Food spawning
Food is a single cell at a random empty position. Pick a random grid coordinate; if the snake is on it, retry. With a 20×20 grid and a short snake, the chance of collision is low — a simple while-loop is acceptable.
let food = spawnFood();
function spawnFood() {
let f;
do {
f = {
x: Math.floor(Math.random() * COLS),
y: Math.floor(Math.random() * ROWS)
};
} while (snake.some(c => c.x === f.x && c.y === f.y));
return f;
}
`snake.some(c => …)` checks if any snake cell matches the food position. If so, the do-while retries with a new random position. On the final levels of a perfect game (snake fills the entire grid), this loop would never terminate — but at that point you’ve technically won.
The game tick
Each tick, calculate the new head position, check for game-over conditions, then move the snake.
function tick() {
const head = {
x: snake[0].x + dir.x,
y: snake[0].y + dir.y
};
// wall collision
if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
return gameOver();
}
// self collision
if (snake.some(c => c.x === head.x && c.y === head.y)) {
return gameOver();
}
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
score++;
food = spawnFood();
} else {
snake.pop();
}
}
The order matters. Compute the new head first. Check collisions before mutating the snake. Add the head to the front (`unshift`), then either consume the food (no `pop`, snake grows) or remove the tail (`pop`, snake stays the same length).
Self-collision detection
The self-collision check is the same `some()` call used in food spawning. If the new head’s coordinates match any existing body cell, the snake has bitten itself. Game over.
One subtle bug: checking `snake.some()` before the unshift includes the tail cell that’s about to be removed. In rare cases (the snake’s head is moving into the cell its tail is about to vacate), this produces a false-positive game over. The fix: exclude the last element from the check, or do the collision check after the unshift but before the pop.
// alternative: exclude the tail tip
if (snake.slice(0, -1).some(c => c.x === head.x && c.y === head.y)) {
return gameOver();
}
For most casual implementations, the simpler version is fine — the false positive only occurs in tight loops.
Drawing
function draw() {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0f0';
snake.forEach(c => {
ctx.fillRect(c.x * CELL, c.y * CELL, CELL - 1, CELL - 1);
});
ctx.fillStyle = '#f00';
ctx.fillRect(food.x * CELL, food.y * CELL, CELL - 1, CELL - 1);
}
Black background, green snake, red food. The `CELL – 1` width creates a 1-pixel gap between cells, making the snake visibly segmented. Drop the `-1` for a solid line.
The game loop
Snake doesn’t need to render at 60fps — the snake moves once per tick, and a tick is 100-150ms. Use setInterval (acceptable here, unlike Pong) or a manual delta-time check on requestAnimationFrame.
let score = 0;
let interval = setInterval(() => {
tick();
draw();
}, 120);
function gameOver() {
clearInterval(interval);
alert(`Game over. Score: ${score}`);
}
120ms per tick is a comfortable starting speed. Slower than 100ms feels slow; faster than 80ms gets challenging fast. As the snake grows, you can decrease the interval to speed up the game.
Speeding up as the game progresses
function tick() {
// ... existing logic ...
if (snakeAteFood) {
score++;
food = spawnFood();
// speed up every 5 points
if (score % 5 === 0) {
clearInterval(interval);
const newSpeed = Math.max(50, 120 - score * 2);
interval = setInterval(() => { tick(); draw(); }, newSpeed);
}
}
}
Cap the minimum interval (around 50ms) so the game doesn’t become unplayable.
Common bugs
- Reverse-into-self. Without the direction-opposite check, pressing the opposite arrow direction immediately ends the game. The `dir.y === 0` guards prevent this.
- Food spawning on the snake. The do-while loop catches this, but if you forget, food appears inside the snake and is uneatable.
- Tail-tip false collision. Mentioned above. Excluding the last cell from the collision check fixes it.
- Double-input per tick. If the player presses two keys quickly between ticks, only the last one registers because `dir` overwrites. Some implementations queue inputs and consume them one per tick.
- Grid drift. If you ever store the snake position in pixels instead of grid cells, floating-point error accumulates. Always store in grid coordinates.
Extending the game
Common additions once the base game works:
- Wraparound walls: Replace the wall game-over with modulo arithmetic — the snake exits one side and re-enters the other.
- Multiple food items: Spawn 3-5 foods at once for a chaos mode.
- Obstacles: Pre-place wall cells the snake must avoid.
- High-score persistence: Save to localStorage so the high score survives page refreshes.
- Touch controls: Swipe gestures for mobile. Listen for touchstart/touchend and compute the swipe direction.
The original context
Snake’s most famous incarnation was on Nokia phones in 1997, but the game predates that by decades — it traces back to the 1976 arcade game Blockade. The Snake game Wikipedia article covers the lineage in detail. For more on Nokia’s role in popularizing the genre, see our Snake history piece, and for variants of the genre, our games like Snake roundup.
Frequently asked questions
Why store the snake in grid coordinates instead of pixels?
Grid coordinates eliminate floating-point drift, simplify collision checks (exact integer equality), and make movement consistent regardless of cell size. Multiply by cell size only at draw time.
How do I prevent the snake from reversing direction?
Block the input if it would reverse the current direction. If the snake is moving horizontally, reject up/down inputs that would create a U-turn. The simplest check: if the current direction is non-zero on an axis, ignore inputs on the same axis.
Why does the snake sometimes “eat itself” when turning quickly?
Two rapid inputs between ticks can register only the second one, which might be opposite to the first. The fix is to queue inputs and consume one per tick, or to validate against the most recent confirmed direction rather than the current dir variable.
What’s a reasonable game speed for Snake?
120-150ms per tick at start. Decrease as the snake grows, capping around 50-60ms. Below that, the game becomes unplayable on most keyboards because input latency dominates.
Can I use requestAnimationFrame instead of setInterval for Snake?
Yes, but you need a manual delta-time check. requestAnimationFrame fires at 60fps; you only want to tick the snake every N frames. Most Snake implementations use setInterval because the fixed-rate behavior is exactly what the game needs.
The bottom line
Snake is the second-easiest classic game to clone, after Pong. The grid model, the array-as-snake representation, and the food-spawn loop are the conceptual building blocks of dozens of other games — Tron, Centipede, even some early Sokoban variants. Build Snake; you’ll have learned the grid-game pattern. For a one-button arcade break between coding sessions, the Chrome Dino game uses the same loop-and-collide architecture in 1D.








