๐ŸŽฎ Get a game ยท $19

๐ŸŸช Free game 4 of 20 ยท copy & paste

Tetris-Style Game in HTML & JavaScript: Free Code + Demo

A falling-blocks puzzle in the style of Tetris: 7 piece shapes, rotation with wall kicks, a ghost piece, next-piece preview, levels and on-screen buttons for phones. Copy the code below, save it as tetris.html and open it in any browser.

Type
Puzzle
Level
Intermediate
Code
173 lines, 1 file
Libraries
None
Works on
Phones & computers
You learn
Matrix rotation, collision checks and clearing lines

โ–ฒ Play it right here. โ† โ†’ move, โ†‘ rotate, โ†“ soft drop, Space hard drop, P pause. On phones use the buttons.

What's in this free Tetris-Style Blocks game

The code (173 lines, one file)

tetris.htmlโฌ‡ Download
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Falling Blocks Game</title>
<!--
  Free HTML falling-blocks (Tetris-style) game โ€” one file, no libraries. Save it as tetris.html and open it in a browser.
  From htmlgamescode.com (free to use in your own projects).
  Controls: โ† โ†’ move, โ†‘ rotate, โ†“ soft drop, Space hard drop, P pause. On phones use the buttons.
-->
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
    background: #1b1446; color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    user-select: none; -webkit-user-select: none; touch-action: none; padding: 8px;
  }
  .top { display: flex; gap: 10px; align-items: stretch; }
  canvas#game { display: block; height: min(calc(100vh - 110px), 520px); aspect-ratio: 1 / 2; border-radius: 12px; background: #120d35; box-shadow: 0 0 0 4px #382c8f; }
  .side { display: flex; flex-direction: column; gap: 8px; width: 92px; font-weight: 700; font-size: 13px; }
  .side div { background: #2a2170; border-radius: 10px; padding: 6px 8px; }
  .side b { display: block; font-size: 20px; }
  canvas#next { width: 100%; aspect-ratio: 1; display: block; }
  .pad { display: flex; gap: 8px; }
  .pad button { width: 56px; height: 48px; border: 0; border-radius: 12px; background: #382c8f; color: #fff; font-size: 22px; box-shadow: 0 4px 0 #20185a; cursor: pointer; }
  .pad button:active { transform: translateY(3px); box-shadow: 0 1px 0 #20185a; }
</style>
</head>
<body>
  <div class="top">
    <canvas id="game" width="240" height="480"></canvas>
    <div class="side">
      <div>Score<b id="score">0</b></div><div>Lines<b id="lines">0</b></div><div>Level<b id="level">1</b></div>
      <div>Next<canvas id="next" width="80" height="80"></canvas></div><div>Best<b id="best">0</b></div>
    </div>
  </div>
  <div class="pad">
    <button data-k="left" aria-label="Left">โ—€</button><button data-k="rotate" aria-label="Rotate">โŸณ</button>
    <button data-k="right" aria-label="Right">โ–ถ</button><button data-k="down" aria-label="Down">โ–ผ</button><button data-k="drop" aria-label="Drop">โค“</button>
  </div>

<script>
  // ---- settings: change these to make it your own ----
  const COLS = 10;               // board width in cells
  const ROWS = 20;               // board height in cells
  const START_DROP = 700;        // ms between automatic drops at level 1
  const SPEED_UP = 60;           // ms faster per level (a new level every 10 lines)
  const MIN_DROP = 90;           // fastest drop speed

  const PIECES = {               // shape + color of each block type
    I: [[[1, 1, 1, 1]], "#3fd8ff"], O: [[[1, 1], [1, 1]], "#ffd23f"], T: [[[0, 1, 0], [1, 1, 1]], "#b46bff"],
    S: [[[0, 1, 1], [1, 1, 0]], "#36d35a"], Z: [[[1, 1, 0], [0, 1, 1]], "#ff4d6d"],
    J: [[[1, 0, 0], [1, 1, 1]], "#3f7bff"], L: [[[0, 0, 1], [1, 1, 1]], "#ff8a3d"]
  };
  const canvas = document.getElementById("game"), ctx = canvas.getContext("2d");
  const nextCtx = document.getElementById("next").getContext("2d");
  canvas.width = COLS * 24; canvas.height = ROWS * 24;
  const CELL = 24, $ = id => document.getElementById(id);
  const load = k => { try { return Number(localStorage.getItem(k)) || 0; } catch { return 0; } };
  const save = (k, v) => { try { localStorage.setItem(k, v); } catch {} };

  let grid, piece, next, score, lines, level, dropTimer, over, paused, best = load("blocks-best");

  const randomPiece = () => { const k = Object.keys(PIECES)[Math.floor(Math.random() * 7)]; return { shape: PIECES[k][0], color: PIECES[k][1] }; };
  const rotate = m => m[0].map((_, c) => m.map(row => row[c]).reverse());   // turn a matrix 90ยฐ clockwise

  function fits(shape, x, y) {
    return shape.every((row, r) => row.every((v, c) => !v || (x + c >= 0 && x + c < COLS && y + r < ROWS && (y + r < 0 || !grid[y + r][x + c]))));
  }

  function spawn() {
    piece = next || randomPiece(); next = randomPiece();
    piece.x = Math.floor((COLS - piece.shape[0].length) / 2); piece.y = -1;
    if (!fits(piece.shape, piece.x, piece.y + 1)) { over = true; if (score > best) { best = score; save("blocks-best", best); } }
    drawNext();
  }

  function newGame() {
    grid = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
    score = 0; lines = 0; level = 1; over = false; paused = false; next = null;
    spawn(); restartTimer(); hud();
  }

  function move(dx) { if (fits(piece.shape, piece.x + dx, piece.y)) piece.x += dx; }
  function turn() {
    const r = rotate(piece.shape);
    for (const kick of [0, -1, 1, -2, 2]) if (fits(r, piece.x + kick, piece.y)) { piece.shape = r; piece.x += kick; return; }
  }
  function down() {
    if (fits(piece.shape, piece.x, piece.y + 1)) { piece.y++; return true; }
    lock(); return false;
  }
  function hardDrop() { let n = 0; while (fits(piece.shape, piece.x, piece.y + 1)) { piece.y++; n++; } score += n * 2; lock(); }

  function lock() {
    piece.shape.forEach((row, r) => row.forEach((v, c) => { if (v && piece.y + r >= 0) grid[piece.y + r][piece.x + c] = piece.color; }));
    if (piece.y < 0) { over = true; if (score > best) { best = score; save("blocks-best", best); } hud(); return; }
    let cleared = 0;
    for (let r = ROWS - 1; r >= 0; r--) if (grid[r].every(Boolean)) { grid.splice(r, 1); grid.unshift(Array(COLS).fill(null)); cleared++; r++; }
    if (cleared) {
      score += [0, 100, 300, 500, 800][cleared] * level; lines += cleared;
      const newLevel = Math.floor(lines / 10) + 1;
      if (newLevel !== level) { level = newLevel; restartTimer(); }
    }
    spawn(); hud();
  }

  function restartTimer() {
    clearInterval(dropTimer);
    dropTimer = setInterval(() => { if (!over && !paused) { down(); draw(); } }, Math.max(MIN_DROP, START_DROP - (level - 1) * SPEED_UP));
  }

  function block(c, x, y, color, size = CELL) {
    c.fillStyle = color; c.fillRect(x * size + 1, y * size + 1, size - 2, size - 2);
    c.fillStyle = "rgba(255,255,255,.3)"; c.fillRect(x * size + 1, y * size + 1, size - 2, 4);
  }

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = "rgba(255,255,255,.04)";
    for (let x = 0; x <= COLS; x++) { ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, canvas.height); ctx.stroke(); }
    grid.forEach((row, r) => row.forEach((v, c) => v && block(ctx, c, r, v)));
    if (!over) {
      let gy = piece.y; while (fits(piece.shape, piece.x, gy + 1)) gy++;            // ghost piece shows where it lands
      piece.shape.forEach((row, r) => row.forEach((v, c) => { if (v && gy + r >= 0) { ctx.fillStyle = "rgba(255,255,255,.12)"; ctx.fillRect((piece.x + c) * CELL + 1, (gy + r) * CELL + 1, CELL - 2, CELL - 2); } }));
      piece.shape.forEach((row, r) => row.forEach((v, c) => v && piece.y + r >= 0 && block(ctx, piece.x + c, piece.y + r, piece.color)));
    }
    if (over || paused) {
      ctx.fillStyle = "rgba(10,6,36,.75)"; ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.font = "900 28px system-ui, sans-serif";
      ctx.fillText(over ? "Game over" : "Paused", canvas.width / 2, canvas.height / 2);
      ctx.font = "700 15px system-ui, sans-serif";
      ctx.fillText(over ? "Tap any button to restart" : "Press P to continue", canvas.width / 2, canvas.height / 2 + 30);
    }
  }

  function drawNext() {
    nextCtx.clearRect(0, 0, 80, 80);
    const s = next.shape, size = 18, ox = (80 - s[0].length * size) / 2 / size, oy = (80 - s.length * size) / 2 / size;
    s.forEach((row, r) => row.forEach((v, c) => v && block(nextCtx, c + ox, r + oy, next.color, size)));
  }
  function hud() { $("score").textContent = score; $("lines").textContent = lines; $("level").textContent = level; $("best").textContent = best; draw(); }

  function act(k) {
    if (over) return newGame();
    if (paused) return;
    if (k === "left") move(-1);
    if (k === "right") move(1);
    if (k === "rotate") turn();
    if (k === "down" && down()) score += 1;
    if (k === "drop") hardDrop();
    hud();
  }

  document.addEventListener("keydown", e => {
    const map = { ArrowLeft: "left", ArrowRight: "right", ArrowUp: "rotate", ArrowDown: "down", " ": "drop" };
    if (map[e.key]) { e.preventDefault(); act(map[e.key]); }
    if (e.key === "p" || e.key === "P") { paused = !paused; draw(); }
  });
  // on-screen buttons repeat while held (except rotate and drop)
  document.querySelectorAll(".pad button").forEach(b => {
    let hold;
    b.addEventListener("pointerdown", e => {
      e.preventDefault(); act(b.dataset.k);
      if (["left", "right", "down"].includes(b.dataset.k)) hold = setInterval(() => act(b.dataset.k), 110);
    });
    ["pointerup", "pointerleave", "pointercancel"].forEach(t => b.addEventListener(t, () => clearInterval(hold)));
  });
  newGame();
</script>
</body>
</html>

How to use it

  1. Click Copy code (or Download).
  2. Paste it into any text editor and save it as tetris.html.
  3. Open the file in Chrome, Safari, Firefox or Edge. That's it, it runs offline.

How the code works

Pieces are small matrices

Each piece is a grid of 1s and 0s. Rotating it is a matrix turn: the columns become the rows, reversed.

Can it move there?

fits() checks every filled cell of the piece against the walls, the floor and the blocks already on the board. Every move or rotation is only applied if it fits.

Clearing lines

When a piece locks, any full row is removed and an empty row is added on top. 1โ€“4 lines score 100, 300, 500 or 800 points times the level.

Make it your own

All the settings are at the top of the script. Change a value, save and reload:

SettingDefaultWhat it does
COLS10board width in cells
ROWS20board height in cells
START_DROP700ms between automatic drops at level 1
SPEED_UP60ms faster per level (a new level every 10 lines)
MIN_DROP90fastest drop speed

Put it on your website

Upload tetris.html next to your page and add this where the game should appear:

<iframe src="tetris.html" width="460" height="620" style="border:0;max-width:100%" title="Tetris-Style Blocks game"></iframe>

FAQ

Is this Tetris-Style Blocks code really free?

Yes. Copy it and use it in your own website, school project or app, change it as much as you like and publish it, even commercially. A link back to HTML Games Code is welcome but not required.

How do I run the Tetris-Style Blocks game?

Copy the code, paste it into a text editor, save the file as tetris.html and double-click it. It opens in your browser and works offline; there is nothing to install.

What are the controls?

โ† โ†’ move, โ†‘ rotate, โ†“ soft drop, Space hard drop, P pause. On phones use the buttons.

Does it work on phones?

Yes. It has touch controls and fits a phone screen, and it works with a keyboard and mouse on a computer.

Can I put it on my website?

Yes. Upload tetris.html and link to it, or show it inside an iframe on any page (see "Put it on your website" above).

Can I turn it into an Android app?

Yes. Any HTML game can be packaged as an Android APK. Our HTML Games APK Builder does it for our premium games, or you can use a tool like htmltoapk.com for this file.

More free HTML games