How to Build a Pong Clone in JavaScript (Tutorial)

high rise buildings during night time

Pong is the canonical first game to clone — two paddles, a ball, a wall to bounce off, and a score. The whole thing runs in 100 lines of JavaScript using the Canvas API. This guide walks through how to build a Pong clone from scratch: a working game loop, keyboard input, ball physics, paddle AI, and a score display. No frameworks, no dependencies, just an HTML file and a script.

Key takeaways

  • Pong needs only the Canvas API, a requestAnimationFrame loop, keyboard input, and a few collision checks.
  • The game loop runs at the browser’s refresh rate via requestAnimationFrame — no manual setInterval needed.
  • Collision detection for Pong is axis-aligned bounding box (AABB) — fast and accurate enough.
  • Ball speed should increase slightly after each paddle hit to keep rounds escalating.
  • Total code: about 100 lines for a complete two-player or vs-AI Pong.

The setup

You need one HTML file with a canvas element and one JavaScript file (or inline script). The canvas is your render surface; everything draws to it via the 2D context.

<canvas id="game" width="800" height="500"></canvas>

That’s the entire HTML you need. The canvas is 800×500, big enough to feel like a real game and small enough to fit in most layouts.

Getting the canvas context

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;

`ctx` is your drawing API. Every shape, line, and text goes through it. `W` and `H` are shortcuts you’ll use constantly in positioning logic.

Game state

Pong has six things to track: two paddles (player and AI), a ball, two scores. Each paddle has a y-position; both have fixed x-positions on the left and right edges. The ball has x, y, vx (x velocity), and vy (y velocity).

const PADDLE_H = 80;
const PADDLE_W = 10;
const BALL_SIZE = 10;

const state = {
  player: { x: 20, y: H/2 - PADDLE_H/2 },
  ai:     { x: W - 30, y: H/2 - PADDLE_H/2 },
  ball:   { x: W/2, y: H/2, vx: 5, vy: 3 },
  score:  { player: 0, ai: 0 }
};

Hard-code the constants up top. Tweaking PADDLE_H or BALL_SIZE during development becomes a single number change.

The game loop

The game loop is the heartbeat of any real-time game. It runs every frame, updates state, then draws. The browser’s native API for this is requestAnimationFrame, which syncs to the display’s refresh rate (usually 60fps).

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

That’s the entire loop. `update` modifies state; `draw` renders state. `requestAnimationFrame(loop)` schedules the next frame and starts the cycle. Don’t use setInterval — it doesn’t sync to the display and produces stutter.

Input handling

Pong needs two keys: up and down (for the player paddle). Track which keys are currently held using an object that listens to `keydown` and `keyup`.

const keys = {};
document.addEventListener('keydown', e => { keys[e.key] = true; });
document.addEventListener('keyup',   e => { keys[e.key] = false; });

Now `keys.ArrowUp` is true while the up arrow is held, false otherwise. In `update`, read these to move the paddle.

function updatePlayer() {
  const speed = 7;
  if (keys.ArrowUp)   state.player.y -= speed;
  if (keys.ArrowDown) state.player.y += speed;
  // clamp inside canvas
  state.player.y = Math.max(0, Math.min(H - PADDLE_H, state.player.y));
}

The clamp prevents the paddle from going off-screen. Without it, holding up indefinitely would send the paddle into negative y values.

The AI paddle

The opponent paddle tracks the ball with a small lag. Simplest possible AI: move toward the ball’s y-position, but cap the speed so the AI is beatable.

function updateAI() {
  const speed = 5;
  const center = state.ai.y + PADDLE_H/2;
  if (state.ball.y < center - 10) state.ai.y -= speed;
  if (state.ball.y > center + 10) state.ai.y += speed;
  state.ai.y = Math.max(0, Math.min(H - PADDLE_H, state.ai.y));
}

The `±10` dead zone prevents jitter — without it, the paddle oscillates around the ball’s y-position. The speed cap (5 vs the player’s 7) makes the AI deliberately slower so the player can win.

Ball physics

Each frame, add velocity to position. Bounce off the top and bottom walls by inverting `vy`. Bounce off paddles by inverting `vx`. Reset on a wall-out and increment the appropriate score.

function updateBall() {
  state.ball.x += state.ball.vx;
  state.ball.y += state.ball.vy;

  // top/bottom walls
  if (state.ball.y <= 0 || state.ball.y + BALL_SIZE >= H) {
    state.ball.vy *= -1;
  }

  // paddle collisions (AABB)
  if (hitPaddle(state.player)) {
    state.ball.vx = Math.abs(state.ball.vx) * 1.05;
  }
  if (hitPaddle(state.ai)) {
    state.ball.vx = -Math.abs(state.ball.vx) * 1.05;
  }

  // out of bounds
  if (state.ball.x < 0) { state.score.ai++; resetBall(); }
  if (state.ball.x > W) { state.score.player++; resetBall(); }
}

The `1.05` multiplier on each paddle hit makes the ball speed up slightly every rally. After 20 hits the ball is moving 2.6x its starting speed — the difficulty curve emerges naturally.

Collision detection

Axis-aligned bounding box collision is two pairs of overlap checks. The ball and the paddle each have a rectangle defined by x, y, width, height. They overlap if and only if:

function hitPaddle(p) {
  return state.ball.x < p.x + PADDLE_W &&
         state.ball.x + BALL_SIZE > p.x &&
         state.ball.y < p.y + PADDLE_H &&
         state.ball.y + BALL_SIZE > p.y;
}

Four comparisons, one boolean. This is the standard collision check for 2D arcade games and is plenty for Pong. More sophisticated collision (swept AABB, continuous collision detection) is unnecessary at these speeds.

Resetting the ball

function resetBall() {
  state.ball.x = W/2;
  state.ball.y = H/2;
  state.ball.vx = (Math.random() > 0.5 ? 5 : -5);
  state.ball.vy = (Math.random() * 6 - 3);
}

Randomize the starting direction so the ball doesn’t always favor one player. The vy randomization makes the angle different each serve.

Drawing

Clear the canvas, draw both paddles, the ball, and the score. Reset the fill style if you want different colors.

function draw() {
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, W, H);

  ctx.fillStyle = '#fff';
  ctx.fillRect(state.player.x, state.player.y, PADDLE_W, PADDLE_H);
  ctx.fillRect(state.ai.x,     state.ai.y,     PADDLE_W, PADDLE_H);
  ctx.fillRect(state.ball.x, state.ball.y, BALL_SIZE, BALL_SIZE);

  ctx.font = '32px monospace';
  ctx.fillText(state.score.player, W/4, 40);
  ctx.fillText(state.score.ai,     3*W/4, 40);
}

Black background, white paddles, white ball. That’s the Pong look. The fonts can be any monospace — courier, consolas, whatever the browser has.

The update function

Tie all the updates together:

function update() {
  updatePlayer();
  updateAI();
  updateBall();
}

One function, three calls. Each call modifies state independently; the draw function reads state at the end of the frame. This separation makes it easy to add features (a pause state, a win condition, sound) without restructuring the loop.

Extending the game

Once the base game runs, common additions:

  • Two-player mode: Replace the AI with a second keyboard input (W/S keys for player 2).
  • Win condition: Stop the game when either score hits 10 and display “Game Over.”
  • Sound: Play a short Audio() clip on each paddle hit.
  • Particle effects: Draw small sparks at paddle hits.
  • Spin: Modify the ball’s vy based on where on the paddle it hit. The classic Pong “english” — a ball hitting the top of the paddle bounces upward, bottom of the paddle bounces downward.

Adding spin

function applySpin(paddle) {
  const center = paddle.y + PADDLE_H/2;
  const hitOffset = (state.ball.y + BALL_SIZE/2) - center;
  state.ball.vy = hitOffset * 0.15;
}

Call this inside the paddle collision check. The further from the paddle’s center, the steeper the vy. This adds skill — the player can aim the ball.

Frame independence

The code above assumes 60fps. On a 120fps monitor the game runs twice as fast; on a 30fps loop it crawls. To make the physics frame-rate independent, multiply velocities by a delta time:

let lastTime = performance.now();
function loop(now) {
  const dt = (now - lastTime) / 16.67; // normalize to 60fps
  lastTime = now;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}

Then multiply movement by `dt` inside `update`. For Pong specifically, the simple non-delta version works on most machines without noticeable issues, so don’t over-engineer until you actually see a problem.

The original context

Pong was the first commercially successful video game, released by Atari in 1972. The original ran on dedicated hardware — no microprocessor, just discrete logic chips. Atari’s history and the original Pong hardware are documented on Wikipedia. For more on the game’s place in arcade history, see our Pong history piece.

Frequently asked questions

Do I need a game framework to build Pong?

No. The Canvas API alone is enough. Phaser, three.js, and other frameworks are overkill for Pong. The 100-line vanilla JS version performs better and teaches you what the framework abstracts away.

Why use requestAnimationFrame instead of setInterval?

requestAnimationFrame syncs to the display’s refresh rate, pauses automatically when the tab is in the background, and produces smoother animation. setInterval drifts over time and runs even on backgrounded tabs, wasting CPU.

What’s AABB collision detection?

Axis-aligned bounding box collision — checking whether two rectangles overlap by comparing their edges. It’s the fastest 2D collision check and works for any non-rotated rectangle. Pong’s paddles and ball are perfect AABB candidates.

How do I make the AI harder?

Increase the AI’s max speed, reduce the dead zone (the ±10 buffer), or have it predict where the ball will land instead of just tracking current y. Predictive AI extrapolates the ball’s trajectory and moves to the predicted intercept.

Can I make Pong multiplayer over the network?

Yes, but it’s significantly harder than the local version. You’d need WebSockets, server-side state authority, and rollback netcode to handle latency. Local two-player Pong (W/S vs arrows) is the easier first multiplayer step.

The bottom line

Pong is the right first game to build because every component is simple, but together they teach the architecture of every action game: a loop, state, input, physics, collisions, render. Once you have Pong running, Breakout is two hours away and Snake is one. For a one-button break between coding sessions, the Chrome Dino game is a minimal-state arcade game in the same lineage.