Test store. Prices are the planned list prices stored as test data, license terms are drafts, and payments go to a Stripe test-mode account. No real money moves.

HTML5 Canvas Games: Tutorial, Game Loop, Tile Maps, Cameras and Examples

An HTML5 canvas game is a JavaScript program that redraws a <canvas> element many times a second in a loop: read input, update the game by the time that passed, draw. Small games like Snake or Breakout need no library. This tutorial builds that loop, then adds spawning, tile maps, a scrolling camera and on-canvas UI, and shows when an engine like Phaser starts to save you time.

An HTML5 canvas game draws its whole world onto one <canvas> element with JavaScript, many times a second. The browser already gives you the three things a game needs: a bitmap to draw on (the Canvas 2D API), a clock (requestAnimationFrame) and input events for keys, mice, touch and gamepads. This guide is a practical HTML5 canvas game tutorial. We build a reusable template, explain the game loop properly, then add the parts real 2D games need: spawning, tile maps, a scrolling camera and UI drawn on the canvas. Every section has code you can paste, and the examples link to complete games you can play and copy.

What makes a canvas game different

Canvas is immediate mode. When you call ctx.fillRect(), the browser paints pixels and forgets them. There is no "player" object on the page to move. To animate, you clear the canvas and draw everything again in its new position, 60 times a second or more. Your JavaScript owns the state (positions, scores, the level), and the canvas is only a picture of that state.

The DOM and SVG work the other way: every element is a live object that the browser keeps, styles and hit-tests for you. That trade-off decides which one fits your game, and we compare the three in canvas vs SVG vs DOM for HTML5 games. For anything with lots of moving objects, such as shooters, platformers, physics puzzles or tycoon games with crowds, canvas wins, because the cost depends on what you draw each frame, not on how many objects the browser has to track.

Games made with HTML5 canvas range from 150-line arcade clones to commercial titles. Engines like Phaser and Three.js also render into a canvas element, through WebGL instead of the 2D context, so all 50 games in our catalog are canvas games under the hood: 41 built on Phaser 4.2.1 and 9 on Three.js r186.

HTML5 canvas game tutorial for beginners: a template

The quickest way to learn HTML5 canvas game development is to start from a small template that already has the right structure, then change the rules. The template below is one file. Save it as game.html, open it in a browser, and move the square with the arrow keys, or by dragging on a phone.

Step 1: the page and the canvas

game.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Canvas game template</title>
  <style>
    html, body { margin: 0; height: 100%; background: #111; overflow: hidden; }
    canvas { display: block; width: 100vw; height: 100vh; touch-action: none; }
  </style>
</head>
<body>
  <canvas id="game"></canvas>
  <script>
    // Steps 2 to 5 go here, in order.
  </script>
</body>
</html>

touch-action: none stops phones from scrolling or zooming while the player drags. The script sits at the end of <body>, so the canvas already exists when it runs.

Step 2: how to access the canvas from your game code

A common beginner question is how to access the canvas from different parts of an HTML5 JavaScript game, or how to make the canvas global. Look it up once, at the top of the script, and keep the reference:

js
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

In a single-file game, top-level const declarations are visible to every function in the script, which is all the "global" you need. When the game grows into several files, use ES modules: export the canvas and context from one module (say, screen.js) and import them wherever you draw. Don't look the element up inside the loop, and avoid window.ctx style globals that any other script can overwrite. If getElementById returns null, the script ran before the browser parsed the canvas: move the script to the end of <body> or load it with defer.

Step 3: size the canvas for sharp pixels

A canvas has two sizes: its CSS size on the page and the size of its drawing buffer. On a phone with a device pixel ratio of 3, a canvas 390 CSS pixels wide needs a buffer 1,170 pixels wide to look sharp. Set the buffer from the CSS size times devicePixelRatio, then scale the context so the rest of your code can think in CSS pixels. MDN's canvas optimization notes describe the same technique.

js
let W = 0, H = 0, dpr = 1;
function resize() {
  dpr = Math.min(window.devicePixelRatio || 1, 2); // capped at 2, see below
  W = canvas.clientWidth;
  H = canvas.clientHeight;
  canvas.width = Math.round(W * dpr);
  canvas.height = Math.round(H * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();

The cap at 2 is a deliberate trade. A 3x buffer has 2.25 times the pixels of a 2x one, and in a moving game most players can't see the difference. Our HTML5 game performance guide explains where the pixels go.

Step 4: state and input

Keep all game data in plain objects. Input handlers only record what the player wants; the update function decides what happens. Pointer Events cover mouse, pen and touch with one code path.

js
const player = { x: 100, y: 100, w: 32, h: 32, speed: 240 }; // speed in CSS px per second
const keys = new Set();
let pointer = null;

addEventListener("keydown", (e) => keys.add(e.key));
addEventListener("keyup", (e) => keys.delete(e.key));
canvas.addEventListener("pointerdown", (e) => { pointer = { x: e.clientX, y: e.clientY }; });
canvas.addEventListener("pointermove", (e) => { if (pointer) pointer = { x: e.clientX, y: e.clientY }; });
addEventListener("pointerup", () => { pointer = null; });

Step 5: update, draw and loop

js
function update(dt) {
  let dx = 0, dy = 0;
  if (keys.has("ArrowLeft")) dx -= 1;
  if (keys.has("ArrowRight")) dx += 1;
  if (keys.has("ArrowUp")) dy -= 1;
  if (keys.has("ArrowDown")) dy += 1;
  if (pointer) { dx = pointer.x - (player.x + player.w / 2); dy = pointer.y - (player.y + player.h / 2); }
  const len = Math.hypot(dx, dy);
  if (len > 0.5) {
    const step = pointer ? Math.min(len, player.speed * dt) : player.speed * dt;
    player.x += (dx / len) * step;
    player.y += (dy / len) * step;
  }
  player.x = Math.max(0, Math.min(W - player.w, player.x)); // keep the player on screen
  player.y = Math.max(0, Math.min(H - player.h, player.y));
}

function draw() {
  ctx.fillStyle = "#111";
  ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = "#4ade80";
  ctx.fillRect(Math.round(player.x), Math.round(player.y), player.w, player.h);
}

let last = performance.now();
function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.1); // seconds since the last frame, capped
  last = now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

That is a complete HTML5 canvas game template: a canvas that is sharp on every screen, input from keyboard, mouse and touch, movement in pixels per second instead of pixels per frame, and a loop. Swap the square for your own rules and you have a game. Our free Snake game code is built on the same skeleton, with a fixed-step loop, swipe controls and a game over screen.

The HTML5 canvas game loop, done properly

requestAnimationFrame calls your function before the browser paints the next frame. According to MDN, the call rate generally matches the display's refresh rate: 60 Hz is the most common, but 75, 120 and 144 Hz screens are widespread. MDN's warning is the most useful sentence in any game loop tutorial: always use the timestamp to work out how far things move, or your game runs faster on high refresh-rate screens.

Three more facts shape a good loop:

  • Browsers pause requestAnimationFrame in background tabs and hidden iframes. When the player comes back, the first delta can be several seconds long. Cap it, or the player teleports through walls.
  • On iPhones in Low Power Mode, Safari caps animation frames at 30 per second. WebKit treats this as intended behavior (WebKit bug 215745), so your game has to play correctly at 30 fps as well as at 120.
  • Older tutorials drive the game with setInterval(update, 20). A timer is not synced to the display, keeps running in some situations where frames don't, and drifts. Use requestAnimationFrame for anything you ship.

Variable step or fixed step?

The template above moves things by speed * dt. That is a variable step, and it's fine for simple games. Its weakness shows up with physics and collisions: a jump that integrates gravity at 30 fps lands at a slightly different height than at 144 fps, and a fast bullet can pass through a thin wall in one long frame. A fixed step solves both. The simulation always advances in equal slices (say 1/60 of a second), and the loop runs as many slices as real time requires.

loop.js
const STEP = 1 / 60; // the simulation always advances 1/60 s at a time
let acc = 0, last = performance.now();

function frame(now) {
  acc += Math.min((now - last) / 1000, 0.25); // ignore huge gaps after a background tab
  last = now;
  while (acc >= STEP) {
    update(STEP); // same step size every time, so results don't depend on frame rate
    acc -= STEP;
  }
  draw(acc / STEP); // 0..1: how far we are between two updates
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

The value passed to draw lets you interpolate: keep each object's previous position, and draw it at prev + (current - prev) * alpha. Without that, a 60-step simulation on a 120 Hz screen draws each position twice and motion looks slightly stepped.

Loop styleHow it worksGood forWatch out for
setIntervalA timer every N msNothing you ship in 2026Drifts, not synced to the display
requestAnimationFrame + variable stepMove by speed × elapsed timeSimple arcade games, menus, effectsPhysics differs between 30 and 144 fps
requestAnimationFrame + fixed stepAccumulate time, update in equal slicesPlatformers, physics, replays, anything competitiveNeeds interpolation for smooth motion on fast screens

Finally, pause the game yourself when the page is hidden, so the player doesn't return to a lost level:

js
document.addEventListener("visibilitychange", () => {
  if (document.hidden) paused = true; // also suspend audio here
});

Spawning items in a 2D canvas game

Most 2D games spawn things: enemies, coins, falling blocks, fish. A spawner needs four decisions: when to spawn, where, how many can exist at once, and when to remove them. The pattern below spawns items just off-screen on a jittered timer and removes them once they leave.

js
const items = [];
let spawnTimer = 0;
const SPAWN_EVERY = 1.2; // seconds between spawns, on average
const MAX_ITEMS = 12;

function updateSpawns(dt) {
  spawnTimer -= dt;
  if (spawnTimer <= 0 && items.length < MAX_ITEMS) {
    spawnTimer = SPAWN_EVERY * (0.7 + Math.random() * 0.6); // jitter so it doesn't feel mechanical
    const fromLeft = Math.random() < 0.5;
    items.push({
      x: fromLeft ? -40 : W + 40,              // start just off-screen
      y: 40 + Math.random() * (H - 80),
      vx: fromLeft ? 80 : -80,
      r: 10 + Math.random() * 20,
    });
  }
  for (const it of items) it.x += it.vx * dt;
  for (let i = items.length - 1; i >= 0; i--) {   // backwards, so removing is safe
    if (items[i].x < -60 || items[i].x > W + 60) items.splice(i, 1);
  }
}

To ramp the difficulty, shorten SPAWN_EVERY as the score rises and raise MAX_ITEMS slowly. On a grid, don't pick random cells until you find an empty one: list the free cells and pick one of those, which never loops forever on a crowded board. For games that create hundreds of short-lived objects, such as bullets and particles, reuse objects from a pool instead of creating new ones every frame, so the garbage collector doesn't cause hitches.

Example mechanics: a fish-eating game

A fish-eating game is a good canvas tutorial project because the whole rule set is size comparison. The player's fish eats anything clearly smaller and dies when something clearly bigger touches it. Collisions are circle against circle: two fish touch when the distance between their centers is less than the sum of their radii.

js
function checkFish(player, fish) {
  for (let i = fish.length - 1; i >= 0; i--) {
    const f = fish[i];
    if (Math.hypot(f.x - player.x, f.y - player.y) >= f.r + player.r) continue; // not touching
    if (f.r < player.r * 0.9) {                      // clearly smaller: eat it
      player.r = Math.sqrt(player.r ** 2 + 0.25 * f.r ** 2); // grow by a share of its area
      fish.splice(i, 1);
    } else if (f.r > player.r * 1.1) {
      return "eaten";                                // clearly bigger: game over
    }                                                // about the same size: nobody eats
  }
  return "ok";
}

The 10% margin keeps the rule readable: players can see which fish are safe. Growing by area rather than radius slows growth naturally as the player gets big. Spawn fish sizes relative to the player's current size, so there are always some to eat and some to avoid, and zoom the camera out as the player grows.

Grid games

Puzzle games such as match-3, 2048 and falling-block games keep their state in a grid. Do all the logic in grid coordinates (column and row) and convert to pixels only when drawing: x = col * CELL, and for input, col = Math.floor(pointerX / CELL). That keeps the rules testable without a canvas, and changing the cell size to fit a phone becomes a one-line change. The free match-3 code and 2048 code pages walk through complete grid games.

Pinball and other physics games

A canvas pinball game is harder than it looks. The ball collides with circles and angled line segments, the flippers rotate, and the ball moves fast enough to pass through a flipper between two frames. You need circle-versus-segment collision, a bounce factor (restitution), and several small physics substeps per frame. Below that level of ambition, write it yourself; above it, use a physics engine. Our HTML5 game physics guide compares the options. One of our 3D racing games, Drift District, uses the Rapier physics engine.

Tile maps: how to use a map array in HTML5 games

A tile map builds a level from a grid of small, reusable images. In code, the map is just an array of numbers, where each number names a tile. Older tutorials write var map = [...]; const is the modern equivalent, and the idea is the same.

js
const TILE = 32, COLS = 12, ROWS = 8;
// 0 = floor, 1 = wall, 2 = water
const map = [
  1,1,1,1,1,1,1,1,1,1,1,1,
  1,0,0,0,0,0,0,0,2,2,0,1,
  1,0,1,1,0,0,0,0,2,2,0,1,
  1,0,0,1,0,0,1,0,0,0,0,1,
  1,0,0,0,0,0,1,0,0,1,0,1,
  1,0,1,0,0,0,0,0,0,1,0,1,
  1,0,0,0,0,1,0,0,0,0,0,1,
  1,1,1,1,1,1,1,1,1,1,1,1,
];
const tileAt = (col, row) =>
  col < 0 || row < 0 || col >= COLS || row >= ROWS ? 1 : map[row * COLS + col]; // outside = wall

A flat array indexed with row * COLS + col is compact and fast; a 2D array (map[row][col]) reads more naturally and works just as well at this size. The tile images live in one atlas image, and each tile index maps to a rectangle in it. Draw only the tiles the camera can see: iterating over a 500 × 500 map every frame to draw a 12 × 8 view wastes almost all the work. MDN's tiles and tilemaps overview covers the same structure and its performance options.

js
const ATLAS_COLS = 8; // tiles per row in the atlas image

function drawMap(cam) {
  const c0 = Math.max(0, Math.floor(cam.x / TILE)), c1 = Math.min(COLS - 1, Math.floor((cam.x + W) / TILE));
  const r0 = Math.max(0, Math.floor(cam.y / TILE)), r1 = Math.min(ROWS - 1, Math.floor((cam.y + H) / TILE));
  for (let r = r0; r <= r1; r++) {
    for (let c = c0; c <= c1; c++) {
      const t = map[r * COLS + c];
      const sx = (t % ATLAS_COLS) * TILE, sy = Math.floor(t / ATLAS_COLS) * TILE;
      ctx.drawImage(atlas, sx, sy, TILE, TILE, c * TILE, r * TILE, TILE, TILE); // world coordinates
    }
  }
}

Layers in a canvas tile game

Real maps use several layers: a ground layer, a decoration layer (rocks, flowers) drawn over it, and an overhead layer (tree tops, roofs) drawn after the characters so they can walk behind things. Keep a separate logic layer for collisions, spawn points and triggers, so the art can change without breaking the rules. Static layers don't need redrawing tile by tile every frame: render them once into an offscreen canvas, or into chunks of 16 × 16 tiles for big maps, and draw each chunk as one image.

How to add a walking border to an HTML5 canvas game

"Walking border" usually means one of two things: keeping the player inside the edges of the world, or stopping them at walls. With the tileAt function above, both are the same problem, because anything outside the map counts as a wall. The reliable way to move against walls is one axis at a time:

js
function moveAndCollide(p, dx, dy) {
  p.x += dx;
  if (hitsWall(p)) p.x -= dx; // undo only the X part
  p.y += dy;
  if (hitsWall(p)) p.y -= dy; // then the Y part
}

function hitsWall(p) {
  const c0 = Math.floor(p.x / TILE), c1 = Math.floor((p.x + p.w - 1) / TILE);
  const r0 = Math.floor(p.y / TILE), r1 = Math.floor((p.y + p.h - 1) / TILE);
  for (let r = r0; r <= r1; r++)
    for (let c = c0; c <= c1; c++)
      if (tileAt(c, r) === 1) return true;
  return false;
}

Resolving X and Y separately lets the player slide along a wall when moving diagonally, instead of sticking to it. For a polished feel, snap the player flush against the tile edge instead of undoing the move, and if anything can move more than one tile per step, split the move into smaller steps.

Tile editors and tile game engines

Typing arrays by hand stops being fun after one level. Tiled is a free map editor that exports JSON with layers, object markers and custom properties. If you use an HTML5 tile game engine, the work is done for you: Phaser loads Tiled maps, draws tilemap layers with culling, and sets collision by tile index. Phaser 4 has both a TilemapLayer and a newer TilemapGPULayer. For diamond-shaped maps, read our guide to isometric HTML5 games.

Cameras and scrolling

As soon as the level is bigger than the screen, you need a camera. Your game objects live in world coordinates (positions in the level); the camera is a rectangle in that world, and drawing subtracts its position to get screen coordinates. A good camera follows the player smoothly and never shows the void past the map's edge.

js
const cam = { x: 0, y: 0 };
const WORLD_W = COLS * TILE, WORLD_H = ROWS * TILE;

function updateCamera(dt) {
  const tx = player.x + player.w / 2 - W / 2; // where the camera wants to be
  const ty = player.y + player.h / 2 - H / 2;
  const k = 1 - Math.exp(-8 * dt);              // smoothing that feels the same at any frame rate
  cam.x += (tx - cam.x) * k;
  cam.y += (ty - cam.y) * k;
  cam.x = Math.max(0, Math.min(WORLD_W - W, cam.x)); // never show outside the map
  cam.y = Math.max(0, Math.min(WORLD_H - H, cam.y));
}

function draw() {
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, W, H);
  ctx.save();
  ctx.translate(-Math.round(cam.x), -Math.round(cam.y)); // whole pixels: no seams between tiles
  drawMap(cam);
  drawEntities();   // player, items, enemies in world coordinates
  ctx.restore();
  drawHud();        // score and buttons in screen coordinates
}

A few details make the difference between a camera that feels right and one that feels cheap:

  • Frame-rate independent smoothing. 1 - Math.exp(-8 * dt) gives the same follow speed at 30, 60 or 144 fps. A fixed 0.1 per frame would follow twice as fast on a 120 Hz screen.
  • Round the translation. Fractional offsets blur pixel art and open thin seams between tiles.
  • Convert input to world space. A tap at screen position (x, y) lands at (x + cam.x, y + cam.y) in the world.
  • Parallax. Draw distant background layers with a fraction of the camera offset (-cam.x * 0.3) for depth.
  • Dead zones and look-ahead. Let the player move a little before the camera reacts, and shift the camera toward the direction of travel in fast games.
  • Screen shake. Add a short random offset to the camera, not to every object, and offer a setting to turn it off.

UI on canvas: HUD, buttons and text

Everything drawn after ctx.restore() in the example above is in screen space, which is where the HUD belongs: the score, lives, a pause button. Position it relative to W and H rather than at fixed coordinates, so it still fits when the phone rotates.

Canvas has no buttons, so you hit-test them yourself. Store each button's rectangle and check pointer positions against it. The WCAG 2.2 target size guidance sets a minimum of 24 by 24 CSS pixels for pointer targets, and thumbs on a phone are happier with more.

js
const buttons = [{ id: "pause", w: 48, h: 48, x: () => W - 60, y: () => 12 }];

canvas.addEventListener("pointerdown", (e) => {
  const rect = canvas.getBoundingClientRect();
  const px = e.clientX - rect.left, py = e.clientY - rect.top; // pointer in canvas CSS pixels
  for (const b of buttons) {
    const bx = b.x(), by = b.y();
    if (px >= bx && px <= bx + b.w && py >= by && py <= by + b.h) onButton(b.id);
  }
});

Text needs care on canvas. It is drawn as pixels, so it doesn't wrap, can't be selected, and is invisible to screen readers. Web fonts must finish loading before you draw with them (await document.fonts.load("24px YourFont")), or the first frames show a fallback font. MDN's performance notes advise avoiding text rendering where you can, so cache text that rarely changes in an offscreen canvas.

That is why many HTML5 canvas games use CSS and HTML for menus, settings and dialogs, layered over the canvas with position: absolute. HTML gives you real buttons with keyboard focus, text wrapping, right-to-left layout and accessibility for free, and CSS transitions animate menus without touching your game loop. Keep the canvas for the play area and the HUD that moves with it. When a game ships in several languages, as most of ours do (six, including Arabic with right-to-left layout), text length and direction change per language, so design UI that measures text instead of assuming widths.

HTML5 canvas game examples you can play and copy

The best way to learn is to read complete, working games. Our free code pages are single HTML files with no libraries, each with a playable demo and a step-by-step tutorial:

ExampleWhat it teaches
SnakeA fixed-step loop, grid movement, input buffering
PongBall movement, paddle collisions, a simple computer opponent
BreakoutCollisions with many objects, lives and levels
Space shooterSpawning enemies and bullets
PlatformerGravity, jumping and solid ground
Maze gameGrid maps and walls
Tower defensePaths, waves and targeting
Endless runnerSpawning obstacles and ramping difficulty

Classic arcade games make the best practice projects. Atari's Pong (1972), Breakout (1976) and Asteroids (1979) have small rule sets and simple shapes, and each one teaches a core technique. Recreate the mechanics freely, but use your own names and art if you publish.

Two other well-known free tutorials are worth knowing. MDN's 2D breakout game using pure JavaScript builds a complete game in short lessons. The W3Schools HTML Game lessons cover the canvas, components, controllers, obstacles and score at a gentle pace. Whatever tutorial you follow, check that it moves things by elapsed time and uses requestAnimationFrame; if it doesn't, apply the loop from this guide.

If you want HTML5 canvas game source code to download, the HTML game code hub has all our free single-file games under the MIT licence. For full commercial projects with levels, art, audio, saves and tests, browse our 50 HTML5 games with source code; every product page has a playable demo.

2D canvas game engines and libraries in 2026

Plain Canvas 2D is the right tool for learning and for small games. As a game grows, you start writing an asset loader, a scene system, a sprite batcher, tilemap rendering, audio handling and input mapping, which is exactly what an engine already contains. Here is where the common options stand as of October 2026:

OptionWhat it isRenders withStatus
Plain Canvas 2DThe browser API used in this guide2D contextBaseline in every current browser
Phaser 4A full 2D game framework: scenes, loader, tilemaps, cameras, physics, audioWebGL; its Canvas renderer still exists but is deprecatedCurrent major version; our 41 2D games use 4.2.1
PixiJS v8A fast 2D renderer; you add the game structureWebGL by default; WebGPU marked experimentalCurrent
EaselJS (CreateJS)A display list over Canvas 2D, popular when games moved off Flash2D contextLast npm release 1.0.2, September 2017
Cocoon (CocoonJS)A service that wrapped canvas games as mobile appsNot applicableShut down by its maker, Ludei, in February 2019

The Phaser v3 to v4 migration guide says the Canvas renderer "should be considered deprecated" and recommends WebGL for all new projects. PixiJS's renderer guide recommends WebGL for production while its WebGPU renderer matures. For a beginner-friendly middle ground, KAPLAY is a JavaScript and TypeScript game library with a simple, function-based API.

Searches for "HTML5 canvas game JavaScript EaselJS" and "HTML5 game development Cocoon canvas" mostly lead to tutorials from the early 2010s. EaselJS still runs, but nothing new has shipped for years, so use it only to maintain an existing project. Cocoon is gone; to turn a canvas game into a mobile app today, wrap it with a tool like Capacitor or ship it as a progressive web app, as explained in our guide to turning an HTML5 game into an Android app.

Signs it's time to move from raw canvas to an engine:

  • You are writing your own sprite sheet animation system or texture atlas loader.
  • You need tilemaps from an editor, several cameras, or physics beyond simple boxes.
  • Audio, scene changes and input remapping are taking more time than gameplay.
  • You need to draw thousands of sprites, where WebGL batching beats Canvas 2D.

Our HTML5 game engines comparison covers the full field, including 3D, and the Phaser tutorial gets you from an empty folder to a running scene. A "canvas online game" with other players also needs a server; see multiplayer HTML5 games for the architecture.

How to make great games with HTML5 canvas

Working code is the start. These are the habits that separate a game people replay from a tech demo:

  1. Respond instantly. Act on pointerdown and keydown, not click. Buffer an input pressed a few frames early (a jump just before landing) instead of dropping it.
  2. Play the same on every screen. Move by elapsed time, and test at 30 fps (Low Power Mode) and on a 120 Hz screen.
  3. Stay readable on a phone. Test at 390 CSS pixels wide. If a projectile or a hazard is hard to see there, it is too small.
  4. Add feedback. Easing, small particles, a brief flash on hit and a little screen shake make actions feel physical. Keep strong effects optional.
  5. Support every input. All 50 of our games accept touch, mouse, keyboard and gamepad. The Gamepad API needs only a few lines once your input is mapped to actions.
  6. Load fast. Our games' initial downloads range from 300 to 750 KB, with a median of about 560 KB. Measure yours as a number, and see the performance guide for how to stay small.
  7. Save progress. Losing a level to a reload is the fastest way to lose a player. Wrap localStorage in try/catch, because some embedded frames block it.
  8. Test in more than one browser. We run every game's end-to-end tests with Playwright on desktop Chromium, Firefox and WebKit, plus emulated Pixel 7 and iPhone 15 profiles. Emulation isn't a real phone, so play on real devices before launch as well.

For the sprites and animation side, read HTML5 game sprites and animation. For the whole path from idea to launch, start with how to make an HTML5 game.

Frequently asked questions

Is HTML5 canvas good for making games?

Yes. Canvas is the standard drawing surface for browser games: Phaser, PixiJS and Three.js all render into a canvas element. The 2D context is enough for most small and medium 2D games, and WebGL on the same element handles thousands of sprites or 3D.

Do I need a library to make an HTML5 canvas game?

No. Snake, Pong, Breakout and simple platformers need only the Canvas 2D API, requestAnimationFrame and input events, as the template above shows. A framework like Phaser starts to pay off when you add sprite animation, tilemaps, sound, scenes and physics.

Should I use the canvas 2D context or WebGL?

Start with the 2D context while you learn. Switch to WebGL, usually through an engine, when you draw many sprites per frame or need shaders and lighting. Phaser 4 recommends WebGL for all new projects and treats its Canvas renderer as deprecated.

Is there an HTML5 canvas game tutorial PDF?

We don't publish a PDF of this guide. Your browser's Print command can save any page as a PDF for offline reading, and our free code examples are single HTML files that run without a connection once saved.

Where can I download HTML5 canvas games with source code?

Free single-file games with tutorials are on our HTML game code page under the MIT licence. Commercial games with complete TypeScript projects, art, audio and tests are in our catalog, licensed for one product or several.

Is EaselJS still a good choice for canvas games?

Not for new projects. Its last npm release, 1.0.2, dates from September 2017. Phaser 4 or PixiJS v8 give you a maintained renderer, WebGL batching and an active community.

We make and sell HTML5 game source code; links to our games are marked as ours.