Hackenshaw logo

Tutorials

your first game — html, css & javascript

$ cat catch-with-html-css-js.md

In this tutorial you build a small game from three empty text files: a paddle you move with the arrow keys, an apple falling from the sky, and a score that goes up every time you catch it. It's the same game as the Scratch and Godot tutorials, so you can compare how each tool thinks.

The finished game: an orange paddle at the bottom, a red apple falling, Score: 3 in the corner.
What you will have at the end. Play it — the arrow keys move the paddle.

You need: a text editor (VS Code is a good free one) and a web browser. Nothing to install, nothing to sign up for. It takes about 45 minutes.

Stuck at any point? Every step below has a run this step link to a working copy, and a link to its files. Open them side by side with yours and look for the difference.

Step 1 — Make the files

Make a new folder called catch. Inside it, make three empty files:

  • index.html — what is on the page
  • style.css — how it looks
  • game.js — what it does

Every web page splits the work this way. Open index.html and type this in:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Catch!</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Catch!</h1>
  <canvas id="game" width="480" height="480"></canvas>
  <script src="game.js"></script>
</body>
</html>

Line by line: the <head> holds things about the page — its title in the browser tab, and a link to the stylesheet. The <body> is what you see: a heading, and a <canvas>, a 480 by 480 pixel rectangle we will draw the game on. The <script> at the very end loads the game code after the canvas exists, so the code can find it.

Save, then double-click index.html to open it in your browser.

A white page with only the heading Catch! in the top-left corner.
Just a heading. The canvas is there — it just has no colour yet, so it is invisible.

▶ run this stepindex.html

Step 2 — Give it some style

Open style.css. We make the page dark, put everything in the middle, and give the canvas a colour so we can see it:

style.css

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #111;
  color: white;
  font-family: monospace;
}

canvas {
  background: #1d2b53;
  border-radius: 12px;
}

A CSS rule is a selector (which things) and, between the braces, properties (what to change). display: flex with align-items and justify-content is the usual way to centre things. Save and reload the browser.

A dark page with the heading centred above a dark blue rounded square.
Now there is a playing field.

▶ run this stepstyle.css

Step 3 — Draw the player

Time for JavaScript. Open game.js:

game.js

// Find the canvas on the page, and get the "pen" we draw with.
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

// The player: where it is and how big it is.
const player = { x: 200, y: 440, width: 80, height: 16 };

// Draw the player once, as an orange rectangle.
ctx.fillStyle = "#ffa300";
ctx.fillRect(player.x, player.y, player.width, player.height);

The first two lines find the canvas and get its context — think of it as the pen we draw with. Then we describe the player as an object: a bundle of named values, x, y, width and height. (Remember the class diagrams from the last workshop? This is a Player with four attributes.)

x y (0, 0) (480, 480) player.x, player.y
The canvas is a grid of pixels. (0, 0) is the top-left corner, and y grows downwards. A rectangle's x and y are its top-left corner.

So x: 200, y: 440 means 200 pixels from the left and 440 down: near the bottom, roughly in the middle. Reload:

The blue playing field with a small orange rectangle near the bottom centre.
The player. Try changing its x, y or width and reloading.

▶ run this stepgame.js

Step 4 — The game loop

A picture that never changes isn't a game. Every game runs the same cycle, over and over, many times a second:

  1. update — move things a tiny bit, check what happened
  2. draw — wipe the screen and paint everything where it is now
  3. repeat

Replace everything in game.js with this. The bright lines are new; the drawing code from step 3 has moved inside draw():

game.js

// Find the canvas on the page, and get the "pen" we draw with.
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

// ----- game state -----
const player = { x: 200, y: 440, width: 80, height: 16 };
const apple = { x: 240, y: 0, radius: 12, speed: 3 };

// ----- update: change the game state a tiny bit -----
function update() {
  apple.y += apple.speed;
}

// ----- draw: wipe the canvas and paint everything where it is now -----
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "#ffa300";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  ctx.fillStyle = "#ff004d";
  ctx.beginPath();
  ctx.arc(apple.x, apple.y, apple.radius, 0, Math.PI * 2);
  ctx.fill();
}

// ----- the game loop: update, draw, repeat - about 60 times a second -----
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

requestAnimationFrame(loop) asks the browser: "next time you draw the screen, call loop again". That's about 60 times a second. Each time, update() moves the apple 3 pixels down, and draw() paints the new picture. 60 slightly different pictures a second looks like movement — the same trick as a flip book.

The apple, a red dot, halfway down the playing field above the paddle.
The apple falls… and keeps falling, straight off the bottom, never to be seen again.

Try this: delete the ctx.clearRect line and reload. Without the wipe, every old picture stays behind and the apple paints a red streak. Put it back.

▶ run this stepgame.js

Step 5 — Again, and again

When the apple leaves the bottom, put it back at the top, somewhere random:

game.js

// Find the canvas on the page, and get the "pen" we draw with.
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

// ----- game state -----
const player = { x: 200, y: 440, width: 80, height: 16 };
const apple = { x: 240, y: 0, radius: 12, speed: 3 };

// Put the apple back at the top, somewhere random across the width.
function resetApple() {
  apple.x = apple.radius + Math.random() * (canvas.width - apple.radius * 2);
  apple.y = -apple.radius;
}

// ----- update: change the game state a tiny bit -----
function update() {
  apple.y += apple.speed;

  // fell off the bottom? start again at the top
  if (apple.y - apple.radius > canvas.height) {
    resetApple();
  }
}

// ----- draw: wipe the canvas and paint everything where it is now -----
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "#ffa300";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  ctx.fillStyle = "#ff004d";
  ctx.beginPath();
  ctx.arc(apple.x, apple.y, apple.radius, 0, Math.PI * 2);
  ctx.fill();
}

// ----- the game loop: update, draw, repeat - about 60 times a second -----
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

Math.random() gives a number between 0 and 1. Multiplied by the width of the canvas (minus the apple on both sides) it becomes a random spot across the top. We start the apple one radius above the canvas, so it slides in instead of popping into view.

The apple falling again, now on the right-hand side.
A new apple, somewhere else each time.

▶ run this stepgame.js

Step 6 — Take control

Now the arrow keys. Add the speed to the player, the key tracking, and the movement at the top of update():

game.js

// Find the canvas on the page, and get the "pen" we draw with.
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

// ----- game state -----
const player = { x: 200, y: 440, width: 80, height: 16, speed: 6 };
const apple = { x: 240, y: 0, radius: 12, speed: 3 };

// ----- input: remember which keys are held down right now -----
const keys = {};
document.addEventListener("keydown", (event) => { keys[event.key] = true; });
document.addEventListener("keyup", (event) => { keys[event.key] = false; });

// Put the apple back at the top, somewhere random across the width.
function resetApple() {
  apple.x = apple.radius + Math.random() * (canvas.width - apple.radius * 2);
  apple.y = -apple.radius;
}

// ----- update: change the game state a tiny bit -----
function update() {
  // move the player while an arrow key is held
  if (keys["ArrowLeft"]) player.x -= player.speed;
  if (keys["ArrowRight"]) player.x += player.speed;

  // ...but never off the edge of the canvas
  player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));

  apple.y += apple.speed;

  // fell off the bottom? start again at the top
  if (apple.y - apple.radius > canvas.height) {
    resetApple();
  }
}

// ----- draw: wipe the canvas and paint everything where it is now -----
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "#ffa300";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  ctx.fillStyle = "#ff004d";
  ctx.beginPath();
  ctx.arc(apple.x, apple.y, apple.radius, 0, Math.PI * 2);
  ctx.fill();
}

// ----- the game loop: update, draw, repeat - about 60 times a second -----
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

The browser tells us when a key goes down and when it comes back up. We write that into keys, so at any moment keys["ArrowLeft"] is true while the left arrow is held. Then every frame, update() checks it and moves the player. Checking every frame is what makes the movement smooth — it's exactly what Scratch's if key pressed inside a forever does.

The Math.max / Math.min line keeps the player on the canvas: never less than 0, never more than the canvas width minus its own width.

The paddle pushed all the way against the right-hand edge of the playing field.
Held right until it hit the wall — and it stops there.

▶ run this stepgame.js

Step 7 — Catch it

Last step: notice when the apple lands on the paddle, and keep score.

game.js

// Find the canvas on the page, and get the "pen" we draw with.
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

// ----- game state -----
const player = { x: 200, y: 440, width: 80, height: 16, speed: 6 };
const apple = { x: 240, y: 0, radius: 12, speed: 3 };
let score = 0;

// ----- input: remember which keys are held down right now -----
const keys = {};
document.addEventListener("keydown", (event) => { keys[event.key] = true; });
document.addEventListener("keyup", (event) => { keys[event.key] = false; });

// Put the apple back at the top, somewhere random across the width.
function resetApple() {
  apple.x = apple.radius + Math.random() * (canvas.width - apple.radius * 2);
  apple.y = -apple.radius;
}

// ----- update: change the game state a tiny bit -----
function update() {
  // move the player while an arrow key is held
  if (keys["ArrowLeft"]) player.x -= player.speed;
  if (keys["ArrowRight"]) player.x += player.speed;

  // ...but never off the edge of the canvas
  player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));

  apple.y += apple.speed;

  // Caught? The apple's bottom has reached the player's top, it hasn't
  // passed the player's bottom yet, and it is between the player's ends.
  const caught =
    apple.y + apple.radius >= player.y &&
    apple.y - apple.radius <= player.y + player.height &&
    apple.x >= player.x &&
    apple.x <= player.x + player.width;

  if (caught) {
    score += 1;
    resetApple();
  }

  // fell off the bottom? start again at the top
  if (apple.y - apple.radius > canvas.height) {
    resetApple();
  }
}

// ----- draw: wipe the canvas and paint everything where it is now -----
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "#ffa300";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  ctx.fillStyle = "#ff004d";
  ctx.beginPath();
  ctx.arc(apple.x, apple.y, apple.radius, 0, Math.PI * 2);
  ctx.fill();

  ctx.fillStyle = "white";
  ctx.font = "20px monospace";
  ctx.fillText("Score: " + score, 12, 30);
}

// ----- the game loop: update, draw, repeat - about 60 times a second -----
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

This is collision detection: checking whether two things overlap. && means and — all four checks have to be true at once:

player.y player.y + height player.x player.x + width apple.y + radius
A catch is four checks at once: the apple's bottom has reached the player's top, it hasn't gone past the player's bottom, and the apple's centre is somewhere between the player's two ends.

If it's a catch, the score goes up and the apple goes back to the top. The score is drawn last, so it sits on top of everything else.

The game with Score: 3 in the top-left corner.
Three caught. You made a game.

▶ play the finished gameindex.htmlstyle.cssgame.js

Look at what you built: an action (move), feedback (the apple disappears), a reward (the score) and a repeat (a new apple). That's the core loop from the last workshop, in 70 lines.

Make it your own

Pick one or more. Each one is a few lines, and each one changes how the game feels.

Faster and faster

Make every catch a bit harder: inside the if (caught) block, add

apple.speed += 0.3;

Three lives

Add let lives = 3; next to score. When the apple falls off the bottom, take one away. At zero, stop the game — wrap everything in update() in if (lives > 0) { … } and draw "GAME OVER" in draw().

A bad apple

Add a second object that costs a point when you catch it. Copy the apple: a badApple object, its own reset, its own catch check, drawn in a different colour.

Sound

Put a sound file next to your code and play it on a catch:

const pop = new Audio("pop.mp3");   // at the top
pop.play();                          // inside if (caught)

Remember the best score

The browser can store a value that survives a reload: localStorage.setItem("best", score) to save it and localStorage.getItem("best") to read it back.

When it doesn't work

  • Open the console. Press F12 (or right-click → Inspect) and look at the Console tab. A typo shows up there in red, with the file and line number.
  • Capitals matter. arrowLeft is not ArrowLeft, and Game.js is not game.js.
  • Brackets come in pairs. Every { needs a }, every ( a ). Your editor highlights the partner when you click next to one.
  • Nothing happens when I press the keys? Click on the page first, so it has the keyboard.
  • Compare with the step that works. Open that step's run this step link and its files, and go line by line.

Same game, other tools: Scratch (blocks, no typing) and Godot (a real game engine).

← back to tutorials