Learn how to build a classic Snake game using only HTML, JavaScript, and ASCII characters.
    _    ____   ____ ___ ___   ____              _          _             _ ____  
   / \  / ___| / ___|_ _|_ _| / ___| _ __   __ _| | _____  (_)_ __       | / ___| 
  / _ \ \___ \| |    | | | |  \___ \| '_ \ / _` | |/ / _ \ | | '_ \   _  | \___ \ 
 / ___ \ ___) | |___ | | | |   ___) | | | | (_| |   <  __/ | | | | | | |_| |___) |
/_/   \_\____/ \____|___|___| |____/|_| |_|\__,_|_|\_\___| |_|_| |_|  \___/|____/ 
This beginner-friendly guide requires no prior game development experience and takes about 30–60 minutes to complete from start to finish, helping you build valuable coding skills and confidence in a fun and creative way.

How to Build Snake in ASCII with JavaScript – A Beginner's Guide

Want to learn how to build games using just text characters?

In this guide, you’ll create a simple version of the classic Snake game using only HTML, JavaScript, and a bit of ASCII art magic.

There’s no need for game engines or fancy frameworks — just your imagination, a text editor, and a web browser.

Whether you're a beginner in coding or a nostalgic fan of text-based games, this is the perfect project to help you understand:
  • How to draw with ASCII in the browser,
  • How to update visuals using JavaScript,
  • And how to handle basic game logic like movement, collision, and scoring.

We'll start from scratch with a tiny snake that moves across the screen, and build it up step by step — adding food, collisions, borders, score, and a restart button — until we have a complete working game, right inside your browser.

🎮 Ready? Let’s bring the snake to life — one # at a time.

🟩 Part 1 – Create a Simple Game Area

Before we dive into snakes and food, let’s start with the basics: how do we display a grid of text in the browser?

In this part, we’ll create a blank play area using HTML and JavaScript. It won’t move or react yet — but it will look like a game board, and that’s a great place to begin.

Step 1: Set up your HTML

Create a new file called snake.html and add the following:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ASCII Snake Game</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
  </style>
</head>
<body>
  <pre id="game"></pre>

  <script>
    const width = 20;
    const height = 10;

    let output = '';
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        output += '.';
      }
      output += '\n';
    }

    document.getElementById('game').textContent = output;
  </script>
</body>
</html>

What this does:

  • We create a grid of 20 columns and 10 rows using . characters.
  • The <pre> tag preserves spacing and newlines, which is perfect for ASCII art.
  • The grid is displayed in green monospace text on a black background — retro terminal style!
Here’s what it looks like in the browser:
Screen
....................
....................
....................
....................
....................
....................
....................
....................
....................
....................

✅ Goal achieved:

You now have a fixed-size grid rendered with JavaScript. No game logic yet — just a clean, simple foundation we’ll build on in the next part.

Next up: drawing the snake and making it move!

🟨 Part 2 – Draw the Snake and Make It Move

Now that we have a simple grid displayed in the browser, it’s time to breathe life into it — by adding a snake that moves.

In this step, we’ll:
  • Represent the snake as an object in code
  • Draw it using # characters
  • Make it move automatically, one step at a time

What you’ll learn

  • How to use arrays to store snake body segments
  • How to redraw the grid every frame
  • How to update the snake’s position with setInterval()

Step-by-step explanation

Let’s build it up slowly, starting with a single snake block that moves across the screen.

Complete HTML for This Step

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 2 – Moving Snake</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
  </style>
</head>
<body>
  <pre id="game"></pre>

  <script>
    const width = 20;
    const height = 10;

    // Snake is an array of {x, y} coordinates
    let snake = [{ x: 5, y: 5 }];

    // Direction the snake moves in (to the right)
    let direction = { x: 1, y: 0 };

    // Game loop: update every 300ms
    setInterval(update, 300);

    function update() {
      // Move snake by creating a new head
      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
      snake.unshift(head);   // Add head
      snake.pop();           // Remove tail (fixed length)

      draw();
    }

    function draw() {
      let output = '';

      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isSnake = snake.some(part => part.x === x && part.y === y);
          output += isSnake ? '#' : '.';
        }
        output += '\n';
      }

      document.getElementById('game').textContent = output;
    }

    draw(); // initial draw
  </script>
</body>
</html>

What happens now?

  • The snake is drawn using a # character
  • It moves one step to the right every 300ms
  • The grid is re-rendered every frame
Here’s what it looks like in the browser:
Screen
....................
....................
....................
....................
....................
....#...............
....................
....................
....................
....................
Each update moves the # further → right, while the tail disappears

What’s next?

So far, the snake just crawls to the right and eventually runs off the screen.

In Part 3, we’ll let you control it with the arrow keys, just like the real game!

🕹️ Part 3 – Control the Snake with Arrow Keys

In Part 2, our snake moved automatically to the right — cool, but not much of a challenge!

Now it's time to give control to the player by listening to the arrow keys and changing the snake’s direction in real-time.

What you’ll learn

  • How to listen for keyboard input with keydown
  • How to change direction based on arrow keys
  • How to prevent the snake from reversing into itself

Complete HTML for This Step

Here’s the updated code with keyboard control added:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 3 – Arrow Key Control</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
  </style>
</head>
<body>
  <pre id="game"></pre>

  <script>
    const width = 20;
    const height = 10;

    let snake = [{ x: 5, y: 5 }];
    let direction = { x: 1, y: 0 }; // Start moving right

    // Listen to arrow key presses
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp'    && direction.y === 0) direction = { x: 0, y: -1 };
      if (e.key === 'ArrowDown'  && direction.y === 0) direction = { x: 0, y: 1 };
      if (e.key === 'ArrowLeft'  && direction.x === 0) direction = { x: -1, y: 0 };
      if (e.key === 'ArrowRight' && direction.x === 0) direction = { x: 1, y: 0 };
    });

    setInterval(update, 200);

    function update() {
      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
      snake.unshift(head);
      snake.pop();
      draw();
    }

    function draw() {
      let output = '';
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isSnake = snake.some(part => part.x === x && part.y === y);
          output += isSnake ? '#' : '.';
        }
        output += '\n';
      }
      document.getElementById('game').textContent = output;
    }

    draw(); // Initial render
  </script>
</body>
</html>

How it works

  • We use document.addEventListener('keydown', ...) to detect arrow key presses.
  • The direction object determines where the snake moves next.
  • We block reverse movement (e.g., going left while moving right) to prevent crashing into itself immediately.
Here’s what it looks like in the browser:
Screen
....................
....................
....................
....................
....................
....#...............
....................
....................
....................
....................
Use your arrow keys ↑ ↓ ← → to steer the snake!

What’s next?

In Part 4, we’ll make the game more exciting by adding food (*) that the snake eats to grow longer!

🍎 Part 4 – Add Food and Grow the Snake

It’s time to make the game more exciting! In this step, we’ll add food to the game area. When the snake eats it, it will grow longer, just like in the classic Snake game.

What you’ll learn

  • How to generate random food on the grid
  • How to detect collisions between the snake and the food
  • How to make the snake grow by not removing the tail segment

Complete HTML for This Step

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 4 – Food and Growth</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
  </style>
</head>
<body>
  <pre id="game"></pre>

  <script>
    const width = 20;
    const height = 10;

    let snake = [{ x: 5, y: 5 }];
    let direction = { x: 1, y: 0 };
    let food = getRandomFood();

    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp'    && direction.y === 0) direction = { x: 0, y: -1 };
      if (e.key === 'ArrowDown'  && direction.y === 0) direction = { x: 0, y: 1 };
      if (e.key === 'ArrowLeft'  && direction.x === 0) direction = { x: -1, y: 0 };
      if (e.key === 'ArrowRight' && direction.x === 0) direction = { x: 1, y: 0 };
    });

    setInterval(update, 200);

    function update() {
      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
      snake.unshift(head);

      // Check if snake eats food
      if (head.x === food.x && head.y === food.y) {
        food = getRandomFood(); // new food
      } else {
        snake.pop(); // only remove tail if no food eaten
      }

      draw();
    }

    function draw() {
      let output = '';
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isHead = x === snake[0].x && y === snake[0].y;
          const isBody = snake.slice(1).some(part => part.x === x && part.y === y);
          const isFood = food.x === x && food.y === y;

          if (isHead) {
            output += 'O';  // Snake head
          } else if (isBody) {
            output += '#';  // Snake body
          } else if (isFood) {
            output += '*';  // Food
          } else {
            output += '.';
          }
        }
        output += '\n';
      }

      document.getElementById('game').textContent = output;
    }

    function getRandomFood() {
      let x, y, collision;
      do {
        x = Math.floor(Math.random() * width);
        y = Math.floor(Math.random() * height);
        collision = snake.some(part => part.x === x && part.y === y);
      } while (collision);
      return { x, y };
    }

    draw();
  </script>
</body>
</html>

How it works

  • The function getRandomFood() finds a random spot that isn’t inside the snake.
  • When the snake's head reaches the food’s position, it grows by not removing the tail.
  • The food is redrawn in a new location with each successful "bite".
  • We’ve also introduced a snake head (O) to clearly show where the snake is going — the rest of the body is still represented with #.
Here’s what it looks like in the browser:
Screen
....................
....................
...............*....
....................
....................
..###O..............
..#.................
..#.................
....................
....................

What’s next?

In Part 5, we’ll add game over logic so the game ends when the snake hits the wall or itself!

💥 Part 5 – Add Game Over Conditions

Our Snake game is almost playable — but there’s still no way to lose. In this step, we’ll add basic collision detection so the game ends if the snake hits the wall or itself.

What you’ll learn

  • How to detect wall collisions
  • How to detect collisions with the snake’s own body
  • How to stop the game loop when the player loses

Complete HTML for This Step

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 5 – Game Over</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
    #message {
      font-family: sans-serif;
      font-size: 18px;
      margin: 10px 0;
      color:red;
    }
  </style>
</head>
<body>
  <pre id="game"></pre>
  <div id="message" style="color: red; font-family: sans-serif; font-size: 18px; margin-top: 10px;"></div>

  <script>
    const width = 20;
    const height = 10;

    let snake = [{ x: 5, y: 5 }];
    let direction = { x: 1, y: 0 };
    let food = getRandomFood();
    let gameInterval = setInterval(update, 200);
    let gameOver = false;

    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp'    && direction.y === 0) direction = { x: 0, y: -1 };
      if (e.key === 'ArrowDown'  && direction.y === 0) direction = { x: 0, y: 1 };
      if (e.key === 'ArrowLeft'  && direction.x === 0) direction = { x: -1, y: 0 };
      if (e.key === 'ArrowRight' && direction.x === 0) direction = { x: 1, y: 0 };
    });

    function update() {
      if (gameOver) return;

      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };

      // Check wall collision
      if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) {
        endGame();
        return;
      }

      // Check self collision
      if (snake.some(part => part.x === head.x && part.y === head.y)) {
        endGame();
        return;
      }

      snake.unshift(head);

      if (head.x === food.x && head.y === food.y) {
        food = getRandomFood();
      } else {
        snake.pop();
      }

      draw();
    }

    function draw() {
      let output = '';
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isHead = x === snake[0].x && y === snake[0].y;
          const isBody = snake.slice(1).some(part => part.x === x && part.y === y);
          const isFood = food.x === x && food.y === y;

          if (isHead) {
            output += 'O';
          } else if (isBody) {
            output += '#';
          } else if (isFood) {
            output += '*';
          } else {
            output += '.';
          }
        }
        output += '\n';
      }

      document.getElementById('game').textContent = output;
    }

    function getRandomFood() {
      let x, y, collision;
      do {
        x = Math.floor(Math.random() * width);
        y = Math.floor(Math.random() * height);
        collision = snake.some(part => part.x === x && part.y === y);
      } while (collision);
      return { x, y };
    }

    function endGame() {
      gameOver = true;
      clearInterval(gameInterval);
      document.getElementById('message').textContent = "Game Over!";
    }

    draw();
  </script>
</body>
</html>

How it works

  • We check if the new head position goes outside the grid — that ends the game.
  • We also check if the snake’s head collides with any part of its body.
  • When either happens, the game stops using clearInterval() and a message appears below the game.
  • To display this message, we added a simple <div> element with the ID message — it's placed below the <pre> and updated by the endGame() function.
Here’s what it looks like in the browser:
Screen
....................
....................
...............*....
......#.............
......#.............
..###O#.............
..#...#.............
..#####.............
....................
....................
Game Over

What’s next?

In Part 6, we’ll add a score counter and a Restart button so you can play again without reloading the page.

🧮 Part 6 – Add Score and Restart Functionality

Now that we have a working Snake game that ends properly, let’s make it more complete by:

  • Displaying a score that increases when you eat food
  • Adding a Restart button so you can quickly try again

What you’ll learn

  • How to update and show the score in real time
  • How to create a restart mechanism using a button
  • How to reset all variables and start fresh

Complete HTML for This Step

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 6 – Score and Restart</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      white-space: pre;
      font-size: 16px;
      margin: 0;
      padding: 20px;
    }
    #score, #message {
      font-family: sans-serif;
      font-size: 18px;
      margin: 10px 0;
    }
    #message {
        color:red;
    }
    #restart {
      padding: 6px 12px;
      font-size: 16px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <!-- Score display -->
  <div id="score">Score: 0</div>
  <pre id="game"></pre>
  <div id="message"></div>
  <button id="restart" onclick="startGame()">Restart</button>

  <script>
    const width = 20;
    const height = 10;

    let snake, direction, food, score, gameInterval, gameOver;

    function startGame() {
      // Reset game state
      snake = [{ x: 5, y: 5 }];
      direction = { x: 1, y: 0 };
      food = getRandomFood();

      // Initialize score
      score = 0;
      gameOver = false;

      document.getElementById('message').textContent = '';
      document.getElementById('score').textContent = 'Score: 0';

      if (gameInterval) clearInterval(gameInterval);
      gameInterval = setInterval(update, 200);

      draw();
    }

    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp'    && direction.y === 0) direction = { x: 0, y: -1 };
      if (e.key === 'ArrowDown'  && direction.y === 0) direction = { x: 0, y: 1 };
      if (e.key === 'ArrowLeft'  && direction.x === 0) direction = { x: -1, y: 0 };
      if (e.key === 'ArrowRight' && direction.x === 0) direction = { x: 1, y: 0 };
    });

    function update() {
      if (gameOver) return;

      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };

      // Wall or self collision
      if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height ||
          snake.some(part => part.x === head.x && part.y === head.y)) {
        endGame();
        return;
      }

      snake.unshift(head);

      if (head.x === food.x && head.y === food.y) {
        // Increase score and update display
        score++;
        document.getElementById('score').textContent = 'Score: ' + score;
        food = getRandomFood();
      } else {
        snake.pop();
      }

      draw();
    }

    function draw() {
      let output = '';
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isHead = x === snake[0].x && y === snake[0].y;
          const isBody = snake.slice(1).some(part => part.x === x && part.y === y);
          const isFood = food.x === x && food.y === y;

          if (isHead) {
            output += 'O';
          } else if (isBody) {
            output += '#';
          } else if (isFood) {
            output += '*';
          } else {
            output += '.';
          }
        }
        output += '\n';
      }

      document.getElementById('game').textContent = output;
    }

    function getRandomFood() {
      let x, y, collision;
      do {
        x = Math.floor(Math.random() * width);
        y = Math.floor(Math.random() * height);
        collision = snake.some(part => part.x === x && part.y === y);
      } while (collision);
      return { x, y };
    }

    function endGame() {
      gameOver = true;
      clearInterval(gameInterval);
      document.getElementById('message').textContent = "Game Over!";
    }

    // Start the game when the page loads
    startGame();
  </script>
</body>
</html>

How it works

  • A score variable is initialized and updated whenever food is eaten.
  • The score is shown in a separate <div id="score"> above the game.
  • A Restart button calls startGame() which resets all variables and restarts the game loop.
Here’s what it looks like in the browser:
Screen
Score: 13
....................
....................
...............*....
......#.............
......#.............
..###O#.............
..#...#.............
..#####.............
....................
....................
Game Over

What’s next?

In the final step, we’ll polish the game by adding a border around the game area, centering the layout, and applying some simple styling to make it feel like a complete game experience.

🏁 Final Step – Polish the Look of Your Snake Game

You've built all the core mechanics of the classic Snake game — now it's time to give it that final polish. In this last step, you'll clean up the visual layout by centering the game on the screen, adding a simple border, and removing the background dots for a cleaner, more authentic ASCII look. With just a few CSS tweaks, your game will go from functional to finished!

What you’ll learn

  • How to style HTML elements using CSS
  • How to center content with Flexbox
  • How to visually separate game elements

Complete HTML for This Step

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Snake Step 7 – Centered and Styled</title>
  <style>
    body {
      background-color: black;
      color: lime;
      font-family: monospace;
      font-size: 16px;
      margin: 0;
      padding: 0;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .container {
      text-align: center;
    }

    #score, #message {
      font-family: sans-serif;
      font-size: 18px;
      margin: 10px 0;
    }

    #message {
        color: red;
    }

    #game {
      padding: 10px;
      border: 2px solid lime;
      display: inline-block;
      white-space: pre;
    }

    #restart {
      margin-top: 10px;
      padding: 6px 12px;
      font-size: 16px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="container">
    <div id="score">Score: 0</div>
    <pre id="game"></pre>
    <div id="message"></div>
    <button id="restart" onclick="startGame()">Restart</button>
  </div>

  <script>
    const width = 20;
    const height = 10;

    let snake, direction, food, score, gameInterval, gameOver;

    function startGame() {
      snake = [{ x: 5, y: 5 }];
      direction = { x: 1, y: 0 };
      food = getRandomFood();
      score = 0;
      gameOver = false;

      document.getElementById('message').textContent = '';
      document.getElementById('score').textContent = 'Score: 0';

      if (gameInterval) clearInterval(gameInterval);
      gameInterval = setInterval(update, 200);

      draw();
    }

    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp'    && direction.y === 0) direction = { x: 0, y: -1 };
      if (e.key === 'ArrowDown'  && direction.y === 0) direction = { x: 0, y: 1 };
      if (e.key === 'ArrowLeft'  && direction.x === 0) direction = { x: -1, y: 0 };
      if (e.key === 'ArrowRight' && direction.x === 0) direction = { x: 1, y: 0 };
    });

    function update() {
      if (gameOver) return;

      const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };

      // Collision with wall or self
      if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height ||
          snake.some(part => part.x === head.x && part.y === head.y)) {
        endGame();
        return;
      }

      snake.unshift(head);

      if (head.x === food.x && head.y === food.y) {
        // Increase score and update display
        score++;
        document.getElementById('score').textContent = 'Score: ' + score;
        food = getRandomFood();
      } else {
        snake.pop();
      }

      draw();
    }

    function draw() {
      let output = '';
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          const isHead = x === snake[0].x && y === snake[0].y;
          const isBody = snake.slice(1).some(part => part.x === x && part.y === y);
          const isFood = food.x === x && food.y === y;

          if (isHead) {
            output += 'O';
          } else if (isBody) {
            output += '#';
          } else if (isFood) {
            output += '*';
          } else {
            output += ' ';
          }
        }
        output += '\n';
      }

      document.getElementById('game').textContent = output;
    }

    function getRandomFood() {
      let x, y, collision;
      do {
        x = Math.floor(Math.random() * width);
        y = Math.floor(Math.random() * height);
        collision = snake.some(part => part.x === x && part.y === y);
      } while (collision);
      return { x, y };
    }

    function endGame() {
      gameOver = true;
      clearInterval(gameInterval);
      document.getElementById('message').textContent = "Game Over!";
    }

    startGame();
  </script>
</body>
</html>

How it works

  • We wrapped all game elements inside a <div class="container"> for easier layout control.
  • The CSS uses flexbox on the body to center everything on the screen, both vertically and horizontally.
  • The game area (<pre id="game">) is styled with a green border to make it pop.
  • We removed the dots (.) from the background and replaced them with spaces to create a cleaner, more minimalist ASCII look.
  • We kept everything minimal and clean, so it still feels like ASCII.

🏁 You Did It!

Congratulations — you've just built a working ASCII Snake game using only HTML, CSS, and JavaScript! Along the way, you’ve learned how to handle keyboard input, draw a grid-based game with text, detect collisions, and style your game to feel polished and playable.

This is a strong foundation for more advanced games or creative ASCII projects. Want to go further? Try adding walls, increasing speed over time, or even keeping a high score.

Until then — enjoy your retro masterpiece.

🎮 Try the Final Game


Thank you for reading and keeping the ASCII spirit alive!