TL;DR
  • This build runs in any web browser - No terminal, no install, just an HTML page and one JavaScript file.
  • Nine buttons make the board. A click handler places your X and calls the computer's move right after.
  • The computer's brain uses the same idea as our Python guide: minimax, a search that checks every possible ending before choosing a move.
  • Paste the finished script into a page with nine buttons and you have a working, unbeatable opponent in your browser.

Why a Browser Changes the Setup

A Python console game asks you to type a number and press enter. A JavaScript version works differently. You click an actual square on a web page, and your X appears right away. The core logic barely changes between the two. The rules, the winning check, and minimax (the search that picks the smartest move) all follow the same rules every tic tac toe game uses.

The Input Changes, the Logic Doesn't

The real difference is how the player and the computer talk to the game. A terminal game waits for you to type a number and hit enter. A browser game waits for a click event instead. Everything past that point, the board, the win check, the computer's move, works the same no matter which one starts it.

No Build Tools Required

You don't need a framework, a bundler, or any tool beyond a text editor and a browser. Everything below is plain JavaScript. It runs the moment the page loads.

JavaScript vs. Python, Side by Side

If you've already built the console version, here's how the two compare:

Piece Python version JavaScript version
Input Type a number, press enter Click a square
Board data List of 9 items Array of 9 items
Event handling Blocking input() loop Event listener callback
Output Printed to the terminal Written into the page

The HTML You Need First

Before the JavaScript can do anything, it needs nine clickable elements to grab onto. A simple grid of buttons works fine:

<div id="board">
  <button class="cell" data-i="0"></button>
  <button class="cell" data-i="1"></button>
  <button class="cell" data-i="2"></button>
  <button class="cell" data-i="3"></button>
  <button class="cell" data-i="4"></button>
  <button class="cell" data-i="5"></button>
  <button class="cell" data-i="6"></button>
  <button class="cell" data-i="7"></button>
  <button class="cell" data-i="8"></button>
</div>

Reading the Markup

Each button carries a data-i attribute. This tells the script which of the nine squares was just clicked. There's no need for nine separate variables. There's no need for nine separate click functions either. One number does the job for all nine buttons.

Add an aria-label for Screen Readers

A screen reader can't guess what an empty button means. Give each one a short aria-label, like aria-label="Row 1, Column 1". That single attribute makes the board usable for players who rely on a screen reader, not just players who can see the grid.

The Same Pattern Scales Up

Nine buttons in a 3-by-3 grid is the smallest version of this idea. Swap in twenty-five buttons and adjust the winning lines, and you get the layout behind our 5x5 board. The HTML barely changes. Only the count and the win-check data grow.

Tracking the Board in JavaScript

Just like a paper board, the whole game lives in one array. This time it holds "X", "O", or null for an empty square. A small function checks all eight winning lines plus the draw case:

const WIN_LINES = [
  [0, 1, 2], [3, 4, 5], [6, 7, 8],
  [0, 3, 6], [1, 4, 7], [2, 5, 8],
  [0, 4, 8], [2, 4, 6]
];

let board = Array(9).fill(null);
let gameOver = false;

function getWinner(state) {
  for (const line of WIN_LINES) {
    const a = line[0], b = line[1], c = line[2];
    if (state[a] && state[a] === state[b] && state[a] === state[c]) {
      return state[a];
    }
  }
  return state.includes(null) ? null : "draw";
}

What getWinner Actually Returns

getWinner does two jobs in one pass. It always returns one of four values:

  • "X" - three X's are in a row.
  • "O" - three O's are in a row.
  • "draw" - the board is full and nobody won.
  • null - the game is still going.

Why One Array Beats Nine Variables

Keeping one array instead of nine loose variables makes the rest of the code shorter. A loop can check every square at once. A single function can pass the whole board into minimax. This same array-of-nine idea scales up if you ever build a bigger board, like the nested grids behind our Ultimate Tic Tac Toe variant.

The Minimax Brain

The computer's move-picking logic follows the same idea as our Python tic tac toe guide, just written the JavaScript way. It uses recursion, a function that calls itself, to look ahead at every possible ending before it picks a move:

function minimax(state, player) {
  const winner = getWinner(state);
  if (winner === "O") return { score: 1 };
  if (winner === "X") return { score: -1 };
  if (winner === "draw") return { score: 0 };

  const moves = [];
  for (let i = 0; i < 9; i++) {
    if (state[i] === null) {
      state[i] = player;
      const next = player === "O" ? "X" : "O";
      const result = minimax(state, next);
      moves.push({ index: i, score: result.score });
      state[i] = null;
    }
  }

  let best = moves[0];
  for (const m of moves) {
    if (player === "O" && m.score > best.score) best = m;
    if (player === "X" && m.score < best.score) best = m;
  }
  return best;
}

function computerMove(state) {
  return minimax(state, "O").index;
}

Maximizing vs. Minimizing

"O" is the maximizing side. The computer wants the highest score it can get. "X" is the minimizing side. The computer assumes you'll play your best against it too. The function calls itself over and over, one move deeper each time, until it reaches a finished game it can actually score.

Speeding It Up with Alpha-Beta Pruning

On a 3-by-3 board, minimax checks every branch fast enough that you'll never notice a delay. Bigger boards are a different story. Alpha-beta pruning is an optional upgrade that skips branches the computer already knows won't change its answer. It's the same search, just with dead ends cut off early. You don't need it here, but it's worth knowing the name once your board grows past nine squares.

Wiring Clicks to Moves

The last piece connects the nine buttons to the logic above. Each button gets an event listener, code that waits for a click and then runs. When you click a square, three things happen in order:

  1. The script reads which square you clicked from its data-i attribute.
  2. It places your X on the board array and on the screen.
  3. It checks whether the game just ended. If not, it hands the turn to computerMove().
const cells = document.querySelectorAll(".cell");

cells.forEach(function (cell) {
  cell.addEventListener("click", function () {
    const i = Number(cell.dataset.i);
    if (board[i] !== null || gameOver) return;

    board[i] = "X";
    cell.textContent = "X";
    if (checkEnd()) return;

    const aiIndex = computerMove(board);
    board[aiIndex] = "O";
    cells[aiIndex].textContent = "O";
    checkEnd();
  });
});

function checkEnd() {
  const result = getWinner(board);
  if (result === "X") alert("You win!");
  else if (result === "O") alert("Computer wins.");
  else if (result === "draw") alert("It's a draw.");
  else return false;
  gameOver = true;
  return true;
}

What Happens When the Game Ends

checkEnd() runs after every move, yours and the computer's. It asks getWinner() for the result. A win or a draw ends the game with an alert() and sets gameOver to true, so no more clicks register.

Touch Screens Work Without Extra Code

A click event fires the same way whether you use a mouse or a finger. You don't need a separate touch listener for phones and tablets. The browser already turns a tap into a click for you.

Turning It Into a Two-Player Game

Want to skip the computer entirely? Remove the computerMove() call and the two lines under it. Now two people can take turns clicking the same nine buttons, similar to how our 2 player game works with no computer opponent at all.

A Browser Game That Can't Lose

What you just built is small, but it's honest about what makes tic tac toe interesting. It's not the graphics. It's not the click animations. It's the decision behind every move. If you want to see the math proving why that decision-making process can't be beaten, our math and game theory page walks through the reasoning without a single line of code. If you'd rather test your own skill against that same unbeatable logic, Hard mode runs the exact idea you just wrote, with better styling than nine plain buttons.

Three Ways to Extend the Build

  • Swap the alert() calls for a message written into the page.
  • Add a restart button so the board resets without a reload.
  • Try a keyboard-only mode, using Tab and Enter instead of clicks, for full accessibility.

Test It Before You Call It Done

Click every square once and confirm it can never place two marks on top of each other. Play three losing games on purpose and check the alert says "Computer wins" every time. A few minutes of manual testing catches bugs a quick glance at the code will miss.

Once you can read the code, tic tac toe stops being just a game you play. It turns into one you can take apart.