Create a simple Tic-Tac-Toe game using HTML, CSS, JavaScript

JavascriptTutorialBeginnersWebDev

Creating a game with javascript is the most fun way to learn. It will keep you motivated and that is crucial for learning complex skills like web development. Furthermore, you can play with your friends, or just show them what you created and they'll be blown away. In today's blogpost we'll create a tic-tac-toe game using only HTML, CSS and Javascript.

Video tutorial

If you would watch a detailed step-by-step video instead you can check out the video I made covering this project on my Youtube Channel:

Implementing the HTML

First in the head section I'll include our css and javascript files that we'll create later. I'll also add a Google font called Itim, which I think is a perfect fit for this game.

    <link rel="stylesheet" href="style.css">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Itim&display=swap" rel="stylesheet">
    <script src="./index.js"></script>

The body of the HTML will be fairly simple. To wrap everything I'll use a main tag, and apply a class of background to it. Inside the main wrapper we'll have five sections.

The first section will only contain our title within a h1.

The second section will display whose turn it is currently. Inside the display we have a span which will contain X or O depending on the current user. We'll apply classes to this span to colorize the text.

The third section is the one that holds the board for the game. It has a container class so we can properly place our tiles. Inside this section we have nine divs which will act as the tiles inside the board.

The fourth section will be responsible to announce the end game result. By default it is empty and we'll modify its content from javascript.

The last section will hold our controls, which contains a reset button.

<main class="background">
        <section class="title">
            <h1>Tic Tac Toe</h1>
        </section>
        <section class="display">
            Player <span class="display-player playerX">X</span>'s turn
        </section>
        <section class="container">
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
            <div class="tile"></div>
        </section>
        <section class="display announcer hide"></section>
        <section class="controls">
            <button id="reset">Reset</button>
        </section>
    </main>

Adding the CSS

I won't go over every single line of CSS, but you can watch the video, or check the source in the project's GitHub repository.

First I'll create the style.css file and remove any browser defined margins and paddings, and set the Google font I included in the HTML for the whole document.

* {
    padding: 0;
    margin: 0;
    font-family: 'Itim', cursive;
}

The next important thing we have to add is the style for our board. We'll use CSS grid to create the board. we can divide the container equally in three by providing 3 times 33% fo r both the columns and the rows. We'll center the container in the middle by setting a maximum width and apply margin: 0 auto;.

.container {
    margin: 0 auto;
    display: grid;
    grid-template-columns: 33% 33% 33%;
    grid-template-rows: 33% 33% 33%;
    max-width: 300px;
}

Next we'll add the style fot the tiles inside the board. We'll apply a little white border, and set a minimum width and height of 100 pixels. We'll center the content using flexbox and setting the justify-content and the align-items to center. We'll give it a large font size and apply cursor: pointer so the user will know that this field is clickable.

.tile {
    border: 1px solid white;
    min-width: 100px;
    min-height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
    cursor: pointer;
}

I'll use two separate colors to better differentiate between the two players. To do this I'll create two utility classes. Player X will have a green color while Player O will have a blue color.

.playerX {
    color: #09C372;
}

.playerO {
    color: #498AFB;
}

These are the key aspects for this project regarding CSS. This is not all the styles that the project uses, so please refer to the project's GitHub repository

Implement the Javascript part

Since we included our javascript file in the <head>, we have to wrap everything in our code between this event handler. This is needed because our script will load before out HTML body will be parsed bythe browser. If you dont want to wrap everything inside this function feel free to add defer into the script tag or move the script tag to the bottom of the body.

window.addEventListener('DOMContentLoaded', () => {
  // everything goes here
});

First we'll save references to our DOM nodes. We'll grap all the tiles using document.querySelectorAll(). We want an array but this function returns a NodeList so we have to convert it to a proper array with Array.from(). We'll also grab a reference to the player display, reset button and the announcer.

const tiles = Array.from(document.querySelectorAll('.tile'));
const playerDisplay = document.querySelector('.display-player');
const resetButton = document.querySelector('#reset');
const announcer = document.querySelector('.announcer');

Next we'll add the global variables that we need to control our game. We'll initialize a board with an Array of nine empty strings. This will hold the X abd O values for every tile on the board. We'll have a currentPlayer that holds the sign of the player whose active in the current turn. The isGameActive variable will be true until someone wins or the game ends in a tie. In these cases we'll set it to false so the remaining tiles will be inactive until a reset. We have three constants which represent end game states. We use these constans to avoid typos.

let board = ['', '', '', '', '', '', '', '', ''];
let currentPlayer = 'X';
let isGameActive = true;

const PLAYERX_WON = 'PLAYERX_WON';
const PLAYERO_WON = 'PLAYERO_WON';
const TIE = 'TIE';

In the next step we'll store all the winning positions on the board. In each sub array we'll store the indexes of the three position that can win the game. So the [0, 1, 2] will represent a case where the first horizontal line is occupied by a player. We'll use this array to decide whether we have a winner or not.

/*
   Indexes within the board
   [0] [1] [2]
   [3] [4] [5]
   [6] [7] [8]
*/

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

Now we'll write a few utility functions. In the isValidAction function we'll decide wether the user wants to perform a valid action or not. If the inner text of the tile is X or O we return false as the action is invalid, otherwise the tile is empty so the action is valid.

const isValidAction = (tile) => {
    if (tile.innerText === 'X' || tile.innerText === 'O'){
        return false;
    }

    return true;
};

The next utility function will be really simple. In this fucntion we'll receive an index as a parameter and set the corresponding element in the board array to be the sign of our current player.

const updateBoard =  (index) => {
   board[index] = currentPlayer;
}

We'll write a little function which will handle the player change. In this function we'll first remove the current player's class from the playerDisplay. The string template literal player${currentPlayer} will become either playerX or playerO depending on the current player. Next we'll use a ternary expression to change the current player's value. If it was X it will be O otherwise it'll be X. Now that we changed the value of our user we need to update the innerText of the playerDisplay and apply the new player class to it.

const changePlayer = () => {
    playerDisplay.classList.remove(`player${currentPlayer}`);
    currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    playerDisplay.innerText = currentPlayer;
    playerDisplay.classList.add(`player${currentPlayer}`);
}

Now we'll write the announer funtion which will announce the end game result. It will receive an end game type and update the innerText of the announcer DOM node based on the result. In the last line we have to remove the hide class, as the announcer is hidden by default until the game ends.

const announce = (type) => {
    switch(type){
       case PLAYERO_WON:
            announcer.innerHTML = 'Player <span class="playerO">O</span> Won';
            break;
       case PLAYERX_WON:
            announcer.innerHTML = 'Player <span class="playerX">X</span> Won';
            break;
       case TIE:
            announcer.innerText = 'Tie';
        }
    announcer.classList.remove('hide');
};

Next we'll write one of the most interesting part of this project the result evaluation. First we'll create a roundWon variable and initialise it with false. Then we'll loop through the winConditions array and check the board for each winning condition. So for example in the second iteration we'll check these values: board[3] (a), board[4] (b), board[5] (c).

We'll also make some optimizations, if any of the fields are empty we'll call continue and skip to the next iteration, because you can't win if there is an empty tile in the win condition. If all the fields are equal then we have a winner, so we set the roundWon to true and break the for loop, because any further iterations would be a wasted computation.

After the loop we'll check the value of the roundWon variable, and if it is true we'll announce a winner and set the game to inactive. If we don't have a winner we'll check wether we have empty tiles on the board and if we don't have a winner and there are no empty tiles left, we announce a tie.

function handleResultValidation() {
  let roundWon = false;
  for (let i = 0; i <= 7; i++) {
    const winCondition = winningConditions[i];
    const a = board[winCondition[0]];
    const b = board[winCondition[1]];
    const c = board[winCondition[2]];
    if (a === "" || b === "" || c === "") {
      continue;
    }
    if (a === b && b === c) {
      roundWon = true;
      break;
    }
  }

  if (roundWon) {
    announce(currentPlayer === "X" ? PLAYERX_WON : PLAYERO_WON);
    isGameActive = false;
    return;
  }

  if (!board.includes("")) announce(TIE);
}

Next we'll handle the user's action. This function will receive a tile and an index as a parameter. This function will be called when the user clicks a tile. First we need to check if it is a valid action or not and we'll also check if the game is active currently or not. If both of them are true, we update the innerText of the tile with the sign of the current player, add the corresponding class and update the board array. Now that everything is updated we have to check whether the game hase ended or not so we call handleResultValidation(). Lastly we have to call the changePlayer method to pass the turn to the other player.

const userAction = (tile, index) => {
  if (isValidAction(tile) && isGameActive) {
    tile.innerText = currentPlayer;
    tile.classList.add(`player${currentPlayer}`);
    updateBoard(index);
    handleResultValidation();
    changePlayer();
  }
};

To get the game working we have to add event listeners to the tiles. We can do that by looping through the array of tiles and add an event listener for each. (For more optimal performace we could only add one event listener to the container and use event bubbling to capture the tile clicks on the parent, but I think for beginners this is easier to understand.)

tiles.forEach( (tile, index) => {
    tile.addEventListener('click', () => userAction(tile, index));
});

There is only one functionality that we miss: resetting the game. For that purpose we'll write a resetBoard function. In this function we set the board to consist of nine empty strings, set the game to active, remove the announcer and change the player back to X (by definition X starts always).

The last thing we have to do is to loop through the tiles and set the innerText back to an empty string, and remove any player specific classes from the tiles.

const resetBoard = () => {
    board = ['', '', '', '', '', '', '', '', ''];
    isGameActive = true;
    announcer.classList.add('hide');

    if (currentPlayer === 'O') {
        changePlayer();
    }

    tiles.forEach(tile => {
        tile.innerText = '';
        tile.classList.remove('playerX');
        tile.classList.remove('playerO');
    });
}

Now we just only have to register this function as a click event handler for the reset button.

resetButton.addEventListener('click', resetBoard);

And this is it, we have a fully functional Tic-Tac-Toe game that you can play with your friend and have a fun time.

If you stuck at any point please watch the video, send a DM to me on Twitter or check the project's GitHub repository.

Happy hacking!

Where can you learn more from me?

I create education content covering web-development on several platforms, feel free to 👀 check them out.

I also create a newsletter where I share the week's or 2 week's educational content that I created. No bull💩 just educational content.

🔗 Links: