Building a Tic Tac Toe Game with HTML, CSS and JavaScript

Creating a web-based Tic Tac Toe game is a great way to learn HTML, CSS, and JavaScript skills while building something fun and interactive. In this tutorial, we'll walk through the process step by step, helping you understand how these technologies work together to create a small, dynamic web application.

Setting Up the HTML Structure

To build the foundation of our Tic Tac Toe game, we start with a simple HTML structure that includes two key elements: the game board and a player turn indicator. We've omitted all non-essential parts for brevity's sake:

<div id="board"></div>
<p id="player-turn">Player X's turn</p>
<script src="index.js"></script>

Styling with CSS

We use CSS to define the structure and appearance of the game borad: The most straightforward approach is to use the display: grid property to define the layout as a 3x3 grid.

#board {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-gap: 0;
    width: 300px;
    height: 300px;
    border-top: 1px solid black;
    border-right: 1px solid black;
    padding: 0;
}

#board > div {
    background-color: white;
    border-left: 1px solid black;
    border-bottom: 1px solid black;
    font-size: 40px;
    text-align: center;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(300px / 3);
    height: calc(300px / 3);
    box-sizing: border-box;
}

Initializing the Game Board with JavaScript

JavaScript is used to generate the game board and handle interactions.

Creating the Game Board


const board = document.getElementById('board');
const boardCells = [];

for (let i = 0; i < 9; i++) {
    const cell = document.createElement('div');
    cell.textContent = '';
    cell.addEventListener('click', handleCellClick);
    cell.setAttribute('data-index', i);
    board.appendChild(cell);
    boardCells.push(cell);
}

Player Interaction and Game Logic

The game logic handles player moves, turn switching, and win condition checks.

Handling Player Moves

let blockClick = false;

function handleCellClick(event) {
    if (blockClick || event.target.textContent !== '') return;

    let index = event.target.getAttribute('data-index');
    boardCells[index].textContent = currentPlayer;

    checkWinner();
    switchTurn();
}

When a cell is clicked, it is marked with the current player's symbol ('X' or 'O'). The game then checks for a winner checkWinner before switching turns calling switchTurn.

Switching Player Turns

// Initialize game state variables
let currentPlayer = 'X';
const playerTurn = document.getElementById('player-turn');

function switchTurn() {
    currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    playerTurn.textContent = `Player ${currentPlayer}'s turn`;
}

The switchTurn function toggles the active player and updates the turn indicator.

Checking for Winners

function checkWinner() {
    const winningCombinations = [
        [0, 1, 2], // horizontal
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6], // vertical
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8], // diagonal
        [2, 4, 6]
    ];

    for (let combination of winningCombinations) {
        if (
            boardCells[combination[0]].textContent &&
            boardCells[combination[0]].textContent === boardCells[combination[1]].textContent &&
            boardCells[combination[0]].textContent === boardCells[combination[2]].textContent
        ) {
            alert(`Player ${boardCells[combination[0]].textContent} wins!`);
            resetGame();
            return;
        }
    }

    if (countEmptyCells() === 0) {
        alert('It\'s a draw!');
        resetGame();
    }
}

The game checks predefined winning combinations stored in the winningCombinations array. Each combination represents a set of three cell indices that form a winning row, column, or diagonal. If all three cells in a combination contain the same symbol ('X' or 'O'), the corresponding player is declared the winner. This is implemented by using a for loop over the winningCombinations array. In the if statement, we first check if the cell is not empty boardCells[combination[0]].textContent && ... and than compare the first cell with the second and the third cell. Since a winning combination can only be formed when all three cells have the same symbol, we only need to check that the first two cells are the same as the third.

If no winning combination is found and all cells are filled, the game announces a draw. To check if there are cells left on the board, we use the function countEmptyCells:

function countEmptyCells() {
    return boardCells.filter(cell => cell.textContent === '').length;
}

The filter method creates a new array with all elements that pass the test implemented by the provided callback function (in this case, cell => cell.textContent === ''). If there are no empty cells left, the result shoud be 0.

The filter method creates a new array with all elements that pass the test implemented by the provided callback function (in this case, cell => cell.textContent === ''). If there are no empty cells left, the length result will be 0.

With these pieces in place, you now have everything you need to create a fully functional Tic Tac Toe game. Here are the source files for your reference:

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</title>
    <style>
        #board {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-gap: 0;
            width: 300px;
            height: 300px;
            border-top: 1px solid black; /* Add top border to the container */
            border-right: 1px solid black; /* Add right border to the container */
            padding: 0;
        }

        #board > div {
            background-color: white;
            border-left: 1px solid black; /* Only left border for cells */
            border-bottom: 1px solid black; /* Only bottom border for cells */
            font-size: 40px;
            text-align: center;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            width: calc(300px / 3);
            height: calc(300px / 3);
            box-sizing: border-box;
        }

        #player-turn {
            font-size: 24px;
            margin-bottom: 20px;
        }
    </style>
</head>

<body>
    <div id="board"></div>
    <p id="player-turn">Player X's turn</p>

    <script src="index.js"></script>
</body>
</html>
JavaScript
// Get the HTML elements
const board = document.getElementById('board');
const playerTurn = document.getElementById('player-turn');

// Initialize the board with empty cells
// [0][1][2]
// [3][4][5]
// [6][7][8]
const boardCells = [];
for (let i = 0; i < 9; i++) {
    const cell = document.createElement('div');
    cell.textContent = '';
    cell.addEventListener('click', handleCellClick);
    cell.setAttribute('data-index', i);
    board.appendChild(cell);
    boardCells.push(cell);
}

let blockClick = false;
function resetGame() {
    blockClick = true;
    setTimeout(() => {
        for (let i = 0; i < 9; i++) {
            boardCells[i].textContent = '';
        }
        blockClick = false;
    }, 1000)

}

// Function to handle cell click
function handleCellClick(event) {
    if (blockClick) return;
    if (event.target.textContent !== '') return;

    // Get the index of the clicked cell
    let index = event.target.getAttribute('data-index');

    // Check if player's turn or opponent's turn
    if (currentPlayer === 'X') {
        boardCells[index].textContent = 'X';
        checkWinner();
        switchTurn();
    } else {
        boardCells[index].textContent = 'O';
        checkWinner();
        switchTurn();
    }
}

// Function to switch turns
function switchTurn() {
    currentPlayer = (currentPlayer === 'X') ? 'O' : 'X';
    playerTurn.textContent = `Player ${currentPlayer}'s turn`;
}

// Function to check winner
function checkWinner() {
    const winningCombinations = [
        [0, 1, 2], // horizonal
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6], // vertical
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8], // diagonal
        [2, 4, 6]
    ];

    for (let combination of winningCombinations) {
        if (
            boardCells[combination[0]].textContent &&
            boardCells[combination[0]].textContent ===
            boardCells[combination[1]].textContent &&
            boardCells[combination[2]].textContent ===
            boardCells[combination[0]].textContent
        ) {
            alert(`Player ${boardCells[combination[0]].textContent} wins!`);
            resetGame();
            return;
        }
    }

    if (countEmptyCells() === 0) {
        alert('It\'s a draw!');
        resetGame();
    }
}

// Function to count empty cells
function countEmptyCells() {
    return boardCells.filter(cell => cell.textContent === '').length;
}

// Initialize game state variables
let currentPlayer = 'X';