Create a breakout game with HTML, CSS, and vanilla JavaScript

We’ll begin by creating the game area using the HTML canvas element.  The  <canvas> element provides access to the Canvas API, which allows us to draw graphics on a webpage.

In your HTML add the code below.

1
<div class="instructions">
2
  <p>
3
    Bounce the ball with the paddle. Use your mouse to move the paddle and
4
    break the bricks.
5
  </p>
6
</div>
7

8
<canvas id="myCanvas" width="640" height="420"></canvas>
9
<button id="start_btn">Start game</button>

The canvas is the area where we will draw our game graphics; it has a width of 640 pixels and a height of 320 pixels. Setting the size in your HTML rather than in  CSS ensures consistency when drawing. If you set the size with CSS only, it may lead to distortion when drawing graphics.

Apply styles

 We will have a few styles to make the game more appealing. Add the following CSS styles.

1
* {
2
    padding: 0;
3
    margin: 0;
4
  }
5
  @import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");
6

7
  body {
8
    display: flex;
9
    align-items: center;
10
    justify-content: center;
11
    flex-direction: column;
12
    gap: 20px;
13
    height: 100vh;
14
    font-family: "DM Mono", monospace;
15
    background-color: #2c3e50;
16
    text-align: center;
17
  }
18
  canvas {
19
    background: #fff;
20
    border: 2px solid #34495e;
21
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
22
  }
23

24
  .instructions {
25
    font-size: 1.3rem;
26
    color: #fff;
27
    max-width: 600px;
28
    margin-bottom: 20px;
29
  }
30

31
  #start_btn {
32
    margin-top: 20px;
33
    padding: 10px 20px;
34
    font-size: 16px;
35
    color: #fff;
36
    background-color: #0c89dd;
37
    border: none;
38
    border-radius: 5px;
39
    cursor: pointer;
40
    font-weight: 500;
41
    font-family: "DM Mono", monospace;
42
  }

Getting started

The first step is to get the reference to the canvas using the getElementById() method. Add the code below.

1
const canvas = document.getElementById("myCanvas");
2
    

Next, get the context. The context (ctx) is where our graphics will be rendered on the canvas.

1
const ctx = canvas.getContext("2d");

Build the paddle

A paddle is the horizontal tool at the bottom of the canvas used to bounce off the ball. 

Define the height and width of the paddle and its start position on the canvas.

1
const paddleHeight = 10;
2
const paddleWidth = 80;
3

4
let paddleStart = (canvas.width - paddleWidth) / 2;

paddleStartStart is the starting point on the X-axis of the canvas where the paddle will start. The position is calculated by half of the canvas width, but we have to account for the space occupied by the paddle, hence the formula paddleStart = (canvas.width - paddleWidth) / 2;

The Canvas API uses the fillRect() method to draw a rectangle using the following formula:

1
fillRect(x, y, width, height)

where :

  • x and y specify the  the starting point of the rectangle
  • width and height specify the size

Create a function named drawPaddle() and draw the paddle using the specified dimensions. The fillStyle property sets the color of the rectangle.

1
function drawPaddle() {
2
    ctx.fillStyle = "#0095DD";
3
    ctx.fillRect(
4
      paddleStart,
5
      canvas.height - paddleHeight,
6
      paddleWidth,
7
      paddleHeight
8
    );  
9
  }

Call the drawPaddle() function to draw the paddle on the canvas.

Mouse movements for the paddle

The next step is to add the ability to move the paddle when a user moves a mouse left and right. Create a mouse-event listener and hook it to a function called movePaddle().

1
document.addEventListener("mousemove", movePaddle, false);

The movePaddle() function will be triggered anytime the user moves the mouse on the canvas. The false argument means that the event will be handled during the bubbling phase, which is the default behavior.

Create the movePaddle() function and add the code below.

1
function movePaddle(e) {
2
    let mouseX = e.clientX - canvas.offsetLeft;
3
    if (mouseX > 0 && mouseX < canvas.width) {
4
      paddleStart = mouseX - paddleWidth / 2;
5
    }
6
  }

In the code above, we get the  X position of the mouse relative to the canvas. The if statement ensures that the paddle doesn’t move beyond the canvas’s bounds. If the condition is met, we will update the paddle’s startX position which will in turn redraw the paddle to the new position of the mouse.

At the moment, the paddle doesn’t move because we are only calling drawPaddle() once. We need to continuously update the paddle’s position based on new mouse movements. To do this, we will implement an animation loop that repeatedly calls drawPaddle(), which will then update the paddle’s position.

Since we will have the same scenario when we draw the ball, let’s create a function called gameLoop which will handle the animation loop.

1
function gameLoop() {
2
    drawPaddle();
3
    requestAnimationFrame(gameLoop);
4
  }

requestAnimationFrame() is a function that helps to create smooth animations. It takes a callback function and executes the callback before the next repaint of the screen. In our case, we are using it to draw the paddle on every repaint. 

Call the gameLoop() function so as to effect the movements:

Update the function to clear the canvas each time the paddle is redrawn in its new position. Add this line of code at the beginning of the function.

1
ctx.clearRect(0, 0, canvas.width, canvas.height);

When you move the mouse over the canvas, the paddle doesn’t get drawn as expected, and you see something like this:

To solve this issue, we need to clear the canvas on every repaint. Update the code as follows:

1
function gameLoop() {
2
   ctx.clearRect(0, 0, canvas.width, canvas.height);
3
    drawPaddle();
4
    requestAnimationFrame(gameLoop);
5
  }

The clearRect() will clear the contents of the canvas. Clearing the canvas is essential to ensure that any previously drawn drawings are removed each time the animation loop runs; this creates an illusion of movement.

Draw the collision ball 

In a breakout game, the ball is used to hit bricks and the user ensures that it bounces back once it hits a brick. Since we don’t have any bricks, yet, we will bounce the ball off the walls of the canvas. Let’s start by drawing the ball. 

The first step is to  define some variables as shown below.

1
let startX = canvas.width / 2;
2
let startY = canvas.height - 100;

startX and startY are the x and y coordinates of the original ball position. 

 To draw the ball, create a function named  drawBall() which looks like this:

1
function drawBall() {
2
    ctx.beginPath();
3
    ctx.rect(startX, startY, 6, 6);
4
    ctx.fillStyle = "blue";
5
    ctx.fill();
6
    ctx.closePath();
7
    
8
    }

In the code above, we are drawing a small  blue rectangle with a width and height of 6 pixels at the specified position. Call the drawBall() function inside the gameLoop() function to ensure the ball is also updated on every repaint.

1
function gameLoop() {
2
     drawPaddle();
3
    requestAnimationFrame(gameLoop);
4
}
5


Simulate ball movements

The next step is to simulate ball movements by updating its position incrementally; this will create an illusion of movement. 

Set the incremental values for both the x and y directions. 

1
let deltaX = 2;
2
let deltaY = 2;

To ensure we understand how the ball moves, we have the following diagram which illustrates how the canvas is measured.

On each repaint, update the position of the ball by incrementing the startX and startY coordinates with the values of deltaX and deltaY; This will make the ball move in a diagonal position

1
function drawBall() {
2
    ctx.beginPath();
3
    ctx.rect(startX, startY, 6, 6);
4
    ctx.fillStyle = "blue";
5
    ctx.fill();
6
    ctx.closePath();
7
    
8
    startX += deltaX;
9
    startY += deltaY; 
10
}

Currently, the ball moves in only one direction and disappears out of the canvas. Let’s ensure it changes direction when it hits the bottom of the canvas. Update the code as shown below.

1
function drawBall() {
2
ctx.beginPath();
3
ctx.rect(startX, startY, 6, 6);
4
ctx.fillStyle = "blue";
5
ctx.fill();
6
ctx.closePath();
7
startX += deltaX;
8
startY += deltaY;
9

10
if (startY + 6 >= canvas.height) {
11
  deltaY = -deltaY;
12
}
13
}

The code startY + 6 >= canvas.height checks if the current position of the ball has reached or exceeded the bottom edge of the canvas. If this condition is true, deltaY = -deltaY reverses the vertical movement of the ball, and it simulates a bouncing effect at the bottom of the canvas.

Left and right wall collisions

If the ball’s x position moves beyond 0 (the left edge of the canvas), reverse its horizontal direction. Similarly, if the ball moves beyond the right edge i.e., exceeds the canvas width, reverse its horizontal direction. 

1
function drawBall() {
2
    ctx.beginPath();
3
    ctx.rect(startX, startY, 6, 6);
4
    ctx.fillStyle = "blue";
5
    ctx.fill();
6
    ctx.closePath();
7
    startX += deltaX;
8
    startY += deltaY;
9
    
10
    if (startY + 6 >= canvas.height) {
11
      deltaY = -deltaY;
12
    }
13
    if (startX < 0 || startX + 6 > canvas.width) {
14
      deltaX = -deltaX;
15
    }
16
}

Top wall collision

From the  canvas illustration, we know that the top-left and top-right of the canvas are (0, 0) and (640, 0), respectively. To ensure the ball doesn’t move beyond the top edge, we use the condition startY < 0. startY < 0 checks if the ball’s y position is above the top edge. If the condition is true, the ball’s vertical direction is reversed and the ball reverses its direction. 

1
function drawBall() {
2
    ctx.beginPath();
3
    ctx.rect(startX, startY, 6, 6);
4
    ctx.fillStyle = "blue";
5
    ctx.fill();
6
    ctx.closePath();
7
    startX += deltaX;
8
    startY += deltaY;
9

10
    if (startY + 6 >= canvas.height) {
11
      deltaY = -deltaY;
12
    }
13
    if (startX < 0 || startX + 6 > canvas.width) {
14
      deltaX = -deltaX;
15
    }
16
    if (startY < 0) {
17
      deltaY = -deltaY;
18
    }
19
  }

So far, we have implemented ball movement, collision detection, and paddle movement. The next step is to create the logic for detecting collisions between the ball and the paddle, ensuring that the ball bounces off the paddle rather than the bottom of the canvas

Ball and paddle collision

For this logic, we want to check if the ball has reached the paddle position; if it touches the paddle, the ball’s direction will be reversed. Create a function called  checkBallPaddleCollision() and add the code below.

1
function checkBallPaddleCollision() {
2
    if (
3
      startY + 6 >= canvas.height - paddleHeight &&
4
      startX + 6 > paddleStart &&
5
      startX < paddleStart + paddleWidth
6
    ) {
7
      deltaY = -deltaY;
8
    }
9
  }

Call the function inside the gameLoop() function.

1
function gameLoop() {
2
    ctx.clearRect(0, 0, canvas.width, canvas.height);
3
    drawBall();
4
    drawPaddle();
5
    checkBallPaddleCollision()
6
    requestAnimationFrame(gameLoop);
7
  }

Draw bricks

Since the canvas is being cleared on every frame (when the paddle or ball is redrawn in a new position), we also need to redraw the bricks on each frame. To do this, we’ll store the brick dimensions in an array called bricks.

This array will store the dimensions of all the bricks needed. We will then create two functions named initializeBricks and DrawBricks(). The first function will store brick dimensions for all the rows and bricks, while the DrawBricks function will be called on every repaint to draw the remaining blocks.

As you can see from the demo, we have 2 rows of bricks with each row containing 7 bricks. Set the following dimensions

1
const brickWidth = 75;
2
const brickHeight = 20;
3
const brickPadding = 10;
4
const brickOffsetTop = 40;
5
const brickOffsetLeft = 30;
6
const numberOfBricks = 7

  • brickWidth :  is thewidth of each brick
  • brickWidth : is the height of each brick
  • brickPadding :This value ensures  even spacing between the bricks.
  • brickOffsetTop : distance between the top of the canvas and the first row of bricks
  • brickOffsetLeft : the space between the bricks and the left side of the canvas
  • numberOfBricks :defines how many bricks are in each row.

Suppose we are only interested in drawing a single row of bricks,  we will create a for loop that runs for the number of bricks. For each brick we will calculate the start point by defining the x coordinate and y coordinates, these values will determine where each brick starts on the canvas.

 Create the initializeBricks() function and add a for loop as shown below

1
function initializeBricks() {
2
for (let i = 0; i < numberOfBricks; i++) {
3
  const brickX = brickOffsetLeft + i * (brickWidth + brickPadding);
4
  const brickY = brickOffsetLeft 
5
           
6
   }
7
 }

brickX will determine the horizontal position of each brick, for example, the brick at index 0 will be placed at 30 pixels on the x-axis and 30 pixels on the y-axis.  For index 1, brickX accounts for the distance from the left of the canvas, plus the width of the previous brick and its padding.

For index 2, brickX accounts for the distance from the left of the canvas plus the width of the previous blocks (2 * blockWidth) plus the padding of the previous blocks (2 * blockRowPadding).

brickY is the vertical position of the bricks; this value doesn’t change since we are drawing a single row. brickX will determine the position of the brick on the canvas , so for example the brick at index 0 will be placed at 30 on the x axis and 30 on the Y axis since the distance from the top doesn’t change. 

The loop will continue until all the bricks have been calculated.

To add other row of bricks, we will adjust the brickY coordinate to account for the height of the previous row and the padding between rows. To accomplish this, let’s set a custom number for the rows.

We will then create an outer loop that will loop through each row and calculate the value of brickY, (this is the distance from the top of the canvas to each brick).

1
function initializeBricks() {
2
for (let row = 0; row < no0fRows; row++) {
3
  const brickY = brickOffsetLeft + row * (brickHeight + brickPadding);
4

5
  for (let i = 0; i < numberOfBricks; i++) {
6
    const brickX = brickOffsetLeft + i * (brickWidth + brickPadding);
7

8
   }
9
 }
10
}

We now have the brick dimensions, create an object for each brick, the object will contain the data below.

  • x:    The x-coordinate of the upper-left corner of the brick  
  •  y    :The y-coordinate of the upper-left corner of the brick  
  •  width    :The width of the brick    
  • height:    The height of the brick
  • color : The color of the brick

In the inner loop statement, rather than drawing the bricks, we will push the dimensions of each brick to the array.

1
function initializeBricks() {
2

3
for (let row = 0; row < no0fRows; row++) {
4
  const brickY = brickOffsetLeft + row * (brickHeight + brickPadding);
5

6
  for (let i = 0; i < numberOfBricks; i++) {
7
    const brickX = brickOffsetLeft + i * (brickWidth + brickPadding);
8
    bricks.push({
9
      x: brickX,
10
      y: brickY,
11
      width: brickWidth,
12
      height: brickHeight,
13
      color: "green",
14
    });
15
  }
16
}
17
}
18
initializeBricks();

If we want to assign each block a random color, we can also do so. Define an array of colors.

1
const colors = ["#0095DD", "#4CAF50", "#FF5733", "#FFC300"];

Update the fillStyle property  as follows: 

1
bricks.push({
2
  x: brickX,
3
  y: brickY,
4
  width: brickWidth,
5
  height: brickHeight,
6
  color: colors[i % colors.length],
7
});

To see the data in table format, you can use console.table which will display the data in tabular format.

The data looks like this:

We now have all the dimensions of the bricks, let’s draw them on the canvas. Create a function called drawBricks() and add the code below:

1
function drawBricks() {
2
    for (let i = 0; i < bricks.length; i++) {
3
      const brick = bricks[i];
4
      ctx.beginPath();
5
      ctx.rect(brick.x, brick.y, brick.width, brick.height);
6
      ctx.fillStyle = brick.color;
7
      ctx.fill();
8
      ctx.closePath();
9
    }
10
  }

In the drawBricks() function, we loop through the bricks array, and for each brick object, we draw a rectangle representing the brick with the specified color.

1
 function gameLoop() {
2
    ctx.clearRect(0, 0, canvas.width, canvas.height);
3
    drawBall();
4
    drawPaddle();
5
    checkBallPaddleCollision();
6
    drawBricks();
7

8
    requestAnimationFrame(gameLoop);
9
  }

Our app now looks like this:

As you can see from the diagram above, the ball can move past the bricks, which is not ideal! We need to ensure that the ball bounces back (reverses direction) when it hits any brick.

Brick and ball collision

The last step in the collision functionality is to check for brick and ball collision detection. Create a function called checkBrickBallCollision().

1
function checkBrickBallCollision() {
2
    
3
}

In this function, we want to loop through the bricks array and check if the ball position is touching any brick. If any collision is detected, the ball’s direction is reversed and the collided brick is removed from the array.

1
 function checkBrickBallCollision() {
2
    for (let i = 0; i < bricks.length; i++) {
3
      const brick = bricks[i];
4
      if (
5
        startX < brick.x + brick.width &&
6
        startX + 6 > brick.x &&
7
        startY < brick.y + brick.height &&
8
        startY + 6 > brick.y
9
      ) {
10
        deltaY = -deltaY;
11
        bricks.splice(i, 1);
12
        break;
13
      }
14
    }
15
  }

Score tracking

The Breakout game isn’t much fun to play without a score. We need to implement a scoring system in which a player will be awarded points every time they hit a brick by bouncing the ball off the paddle. Create a variable called score and initialize it to 0.

Create a function called UpdateSCore() and add the code below that adds the score value at the top left corner of the canvas. 

1
function updateScore() {
2
    ctx.font = "16px Arial";
3
    ctx.fillText("Score: " + score, 10, 20);
4
  }

Call the function in the gameLoop() function.

1
function gameLoop() {
2
    ctx.clearRect(0, 0, canvas.width, canvas.height);
3
    updateScore();
4
    drawBricks();
5
    drawPaddle();
6
    drawBall();
7

8
    checkBallPaddleCollision();
9
    checkBrickBallCollision();
10
    
11
  }

The score should be updated every time the ball hits a brick. Update the checkBrickBallCollision() to include the functionality.

1
function checkBrickBallCollision() {
2
    for (let i = 0; i < bricks.length; i++) {
3
      const brick = bricks[i];
4

5
      if (
6
        startX < brick.x + brick.width &&
7
        startX + 6 > brick.x &&
8
        startY < brick.y + brick.height &&
9
        startY + 6 > brick.y
10
      ) {
11
        deltaY = -deltaY;
12
        bricks.splice(i, 1);
13
        //update points
14
        score += 10;
15
        break;
16
      }
17
    }
18
  }

Game win logic and reset game

The final step is to add win logic. To win the game, the player must hit all the bricks without missing the paddle. If the ball misses the paddle, the game should reset, and a notification should be displayed.  Update the if statement in the drawBall() function where we check if the ball hits the bottom of the canvas.

1
function drawBall() {
2
    // the rest of the code
3
    }
4
    if (startY + 6 >= canvas.height) {
5
      deltaY = -deltaY;
6
      alert("Try Again");
7
     
8
    }
9
}

Create a variable gameWon that will keep track of whether the player has won the game or not.

In the gameLoop() function, add an if statement to check if all the bricks have been hit by the ball. If so, set the gameWon variable to true and display a notification. Additionally, update the requestAnimationFrame(gameLoop) call to ensure it continues running as long as the game has not been won.

1
function gameLoop() {
2
  if (bricks.length === 0 && !gameWon) {
3
    gameWon = true;
4
    alert("You Won: Play Again");
5
    resetGame();
6
  }
7
  ctx.clearRect(0, 0, canvas.width, canvas.height);
8
  updateScore();
9
  drawBricks();
10
  drawPaddle();
11
  drawBall();
12

13
  checkBallPaddleCollision();
14
  checkBrickBallCollision();
15
  if (!gameWon) {
16
    requestAnimationFrame(gameLoop);
17
  }
18
}

The resetGame() function looks like this;

1
function resetGame() {
2
    score = 0;
3
    startX = canvas.width / 2;
4
    startY = canvas.height - 100;
5
    deltaX = -2;
6
    deltaY = -2;
7
    initializeBricks();
8

9
}

In this function, we are resetting the score to 0, repositioning the ball to its original starting position, initializing the brick layout, and reversing the ball’s direction; this ensures the game is reset  to allow the player to try again.

Finally, in the initializeBricks() function, reset the brick array’s length to 0 to ensure no leftover bricks from the previous game state.

1
function initializeBricks() {
2
    bricks.length = 0;
3
    // the rest of the code
4
  }

Start game

The final step is to use a button to start the game so that the game doesn’t automatically start when you open the app.  Get a reference to the start game button and add a click event listener that calls the gameLoop() function. 

1
document
2
    .getElementById("start_btn")
3
    .addEventListener("click", function () {
4
      gameLoop();
5
    });

Our final game

Here is a reminder of what we’ve built!

Conclusion

That was quite a journey! We’ve tackled a range of concepts in this game, from drawing a simple canvas to implementing moving objects, collision detection, and managing game states. Now you have a fully functioning Breakout game. To take it to the next level, consider adding features like high score tracking and sound effects to make the game even more engaging.