Tic-Tac-Toe Game Using HTML, CSS and JavaScript

Beginner 2 views Oct 10, 2026

Tic-Tac-Toe is the classic first game for every programmer. The board is tiny, the rules fit in one sentence, and yet building it teaches you how to store game state, check for a winner and write a basic computer opponent. In this tutorial you will build a Tic-Tac-Toe Game that you can play with a friend or against the computer, with a score board that keeps count of wins and draws.

The whole board is just an array of nine items, and the winner is found by checking eight lines. Once you understand that idea, you can build many other board games.

What You'll Learn

By the end of this tutorial you will know:

  • How to store a 3 ร— 3 board in a simple array of nine items
  • How to check all eight winning lines with a loop
  • How to switch turns between two players
  • How to write a small computer player that wins, blocks and picks the centre
  • How to highlight the winning cells with a CSS class
  • How to disable the board when the game is over

Features of This Tic-Tac-Toe Game

  • Two player mode on the same screen
  • Play against the computer (checkbox)
  • Win, draw and turn messages
  • Winning line is highlighted
  • Score board for X, O and draws
  • New game and reset score buttons
  • Works with mouse and touch

Prerequisites

You only need the basics: how to write a simple HTML page, a few CSS rules, and what a JavaScript variable and function are. Along with that, have a code editor such as Visual Studio Code and any modern browser ready. No frameworks, no build tools and no installs.

Project Structure

Create a new folder called tic-tac-toe-game and add three empty files inside it:

PLAINTEXT
tic-tac-toe-game/
โ”œโ”€โ”€ index.html   โ†’ the page structure
โ”œโ”€โ”€ style.css    โ†’ the look and layout
โ””โ”€โ”€ script.js    โ†’ the behaviour

Step 1: Create the HTML Structure

Create index.html. The board is nine buttons inside a grid. Using buttons (not divs) gives you keyboard support for free.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tic-Tac-Toe Game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="game">
    <h1>Tic-Tac-Toe</h1>
    <p id="status" class="status">Xโ€™s turn</p>

    <div class="board" 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>

    <div class="score">
      <span>X: <strong id="sx">0</strong></span>
      <span>Draws: <strong id="sd">0</strong></span>
      <span>O: <strong id="so">0</strong></span>
    </div>

    <label class="cpu"><input type="checkbox" id="cpu"> Play against the computer (computer is O)</label>

    <div class="btns">
      <button id="newBtn">New game</button>
      <button id="resetBtn" class="ghost">Reset score</button>
    </div>
  </main>

  <script src="script.js"></script>
</body>
</html>

Step 2: Add the CSS Styling

Create style.css. A CSS grid with three equal columns makes the board. The X and O colours are different so players can see the marks at a glance.

CSS
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Segoe UI", Arial, sans-serif;
  background: #eef1f8;
  color: #1f2937;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.game {
  width: 100%;
  max-width: 380px;
  background: #fff;
  padding: 26px;
  border-radius: 16px;
  box-shadow: 0 10px 30px rgba(31, 41, 55, 0.12);
  text-align: center;
}

h1 { font-size: 1.6rem; }

.status {
  margin: 8px 0 16px;
  font-weight: 600;
  color: #4f46e5;
  min-height: 1.4em;
}

.board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.cell {
  aspect-ratio: 1;
  font-size: 2.8rem;
  font-weight: 700;
  border: none;
  border-radius: 12px;
  background: #eef1f8;
  cursor: pointer;
  transition: background 0.15s;
}

.cell:hover:not(:disabled) { background: #dfe4f3; }
.cell.x { color: #4f46e5; }
.cell.o { color: #e11d48; }
.cell.win { background: #bbf7d0; }

.score {
  display: flex;
  justify-content: space-between;
  margin: 18px 0 10px;
  font-size: 0.95rem;
}

.cpu {
  display: block;
  font-size: 0.85rem;
  color: #4b5563;
  margin-bottom: 14px;
}

.btns { display: flex; gap: 10px; }

.btns button {
  flex: 1;
  padding: 11px;
  border: none;
  border-radius: 10px;
  background: #4f46e5;
  color: #fff;
  font-size: 0.95rem;
  cursor: pointer;
}

.btns .ghost {
  background: #e5e7eb;
  color: #374151;
}

Step 3: Add the JavaScript

Create script.js. The array board holds "X", "O" or an empty string for each cell. Every move updates the array first, then redraws the buttons.

JAVASCRIPT
const 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]
];

const cells = document.querySelectorAll('.cell');
const statusEl = document.getElementById('status');
const cpuBox = document.getElementById('cpu');

let board, turn, over;
const score = { X: 0, O: 0, D: 0 };

function newGame() {
  board = Array(9).fill('');
  turn = 'X';
  over = false;
  cells.forEach(c => {
    c.textContent = '';
    c.disabled = false;
    c.className = 'cell';
  });
  statusEl.textContent = 'Xโ€™s turn';
}

function findWinner(b) {
  for (const line of LINES) {
    const [a, c, d] = line;
    if (b[a] && b[a] === b[c] && b[a] === b[d]) {
      return { player: b[a], line: line };
    }
  }
  return null;
}

function play(i) {
  if (over || board[i]) return;

  board[i] = turn;
  cells[i].textContent = turn;
  cells[i].classList.add(turn.toLowerCase());

  const result = findWinner(board);
  if (result) {
    over = true;
    result.line.forEach(k => cells[k].classList.add('win'));
    score[result.player]++;
    statusEl.textContent = result.player + ' wins!';
    finish();
    return;
  }
  if (board.every(Boolean)) {
    over = true;
    score.D++;
    statusEl.textContent = 'Itโ€™s a draw';
    finish();
    return;
  }

  turn = turn === 'X' ? 'O' : 'X';
  statusEl.textContent = turn + 'โ€™s turn';

  if (cpuBox.checked && turn === 'O') {
    setTimeout(computerMove, 350);
  }
}

function finish() {
  cells.forEach(c => (c.disabled = true));
  document.getElementById('sx').textContent = score.X;
  document.getElementById('so').textContent = score.O;
  document.getElementById('sd').textContent = score.D;
}

function bestSpot(mark) {
  // a cell that would complete a line for this mark
  for (const line of LINES) {
    const marks = line.map(i => board[i]);
    if (marks.filter(m => m === mark).length === 2 && marks.includes('')) {
      return line[marks.indexOf('')];
    }
  }
  return -1;
}

function computerMove() {
  if (over) return;
  let spot = bestSpot('O');            // 1. win if possible
  if (spot < 0) spot = bestSpot('X');  // 2. otherwise block
  if (spot < 0 && !board[4]) spot = 4; // 3. take the centre
  if (spot < 0) {                      // 4. any free cell
    const free = board.map((v, i) => (v ? -1 : i)).filter(i => i >= 0);
    spot = free[Math.floor(Math.random() * free.length)];
  }
  play(spot);
}

cells.forEach(c => c.addEventListener('click', () => {
  if (cpuBox.checked && turn === 'O') return; // wait for the computer
  play(Number(c.dataset.i));
}));

document.getElementById('newBtn').addEventListener('click', newGame);
document.getElementById('resetBtn').addEventListener('click', () => {
  score.X = score.O = score.D = 0;
  finish();
  newGame();
});

newGame();

How This Tic-Tac-Toe Game Works

  • The board array is the single source of truth. The buttons only show what the array says.
  • findWinner() loops over the eight winning lines. A line wins when its three cells hold the same non-empty mark.
  • Turns flip between X and O after every valid move. Clicking a taken cell does nothing because play() returns early.
  • The computer follows four simple rules in order: win if you can, block the opponent, take the centre, otherwise pick a random free cell. It is not unbeatable but it is not silly either.
  • Game over disables all nine buttons so no more moves can be made until you press New game.

Customization Guide

1. Make the computer play first

Set turn = "O" in newGame() and call computerMove() when the checkbox is on.

2. Change the colours

Edit .cell.x, .cell.o and .win in the CSS to match your own theme.

Future Enhancements

Once the basic version works, here are some ideas to practise with:

  • Add an unbeatable computer with the minimax algorithm
  • Let players type their names
  • Save the score in localStorage
  • Add a larger 4 ร— 4 board

Troubleshooting Guide

Clicking a cell does nothing

Check that every button has a data-i attribute from 0 to 8 and that the script is loaded after the HTML.

The computer never moves

Make sure the checkbox has the id cpu and that the click handler is not returning early because it is the computerโ€™s turn.

Related Mini Projects

Want more practice? Try these beginner projects next:

Conclusion

You built a complete two player game with win detection and a simple computer opponent. The same pattern of state array, render and rule check is the base of many games, from Connect Four to Sudoku.

Technologies Used

HTML5 CSS3 JavaScript

Live Output

See the project in action below. This is the live preview of the code you just learned.

Output Preview