How to Build a Tic-Tac-Toe Game with Vanilla JavaScript

Here’s the tic-tac-toe game we’re going to be coding. Click on each square to place the pieces, and try to get three in a row:

HTML Structure

The HTML Structure will consist of the following elements:

  • A board which will feature 9 div elements representing the 3×3 grid
  • A Bootstrap modal will be shown when the game ends. The modal will display the winner.
  • A button which when clicked will restart the game

Create a container containing the board and the restart button with Bootstrap.

1
<div class="container">
2
  <h1 class="text-center mt-5">Tic Tac Toe Game</h1>
3
  <div class="board" id="board">
4
    <div class="cell" data-cell></div>
5
    <div class="cell" data-cell></div>
6
    <div class="cell" data-cell></div>
7
    <div class="cell" data-cell></div>
8
    <div class="cell" data-cell></div>
9
    <div class="cell" data-cell></div>
10
    <div class="cell" data-cell></div>
11
    <div class="cell" data-cell></div>
12
    <div class="cell" data-cell></div>
13
  </div>
14
  <!-- <div class="text-center mt-3">
15
    <h3 id="winnner"></h3>
16
  </div> -->
17
  <div class="text-center mt-3">
18
    <button id="restartButton" class="btn btn-primary">Restart Game</button>
19
  </div>
20
</div>

Below the container, add the results modal.

1
<div
2
  class="modal fade"
3
  id="resultModal"
4
  tabindex="-1"
5
  aria-labelledby="resultModalLabel"
6
  aria-hidden="true"
7
>
8
  <div class="modal-dialog">
9
    <div class="modal-content">
10
      <div class="modal-header">
11
        <h5 class="modal-title" id="resultModalLabel">Game Over</h5>
12
        <button
13
          type="button"
14
          class="close"
15
          data-dismiss="modal"
16
          aria-label="Close"
17
        >
18
          <span aria-hidden="true">&times;</span>
19
        </button>
20
      </div>
21
      <div class="modal-body" id="results"></div>
22
      <div class="modal-footer">
23
        <button
24
          type="button"
25
          class="btn btn-primary"
26
          data-dismiss="modal"
27
          id="playBtn"
28
        >
29
          Play Again
30
        </button>
31
      </div>
32
    </div>
33
  </div>
34
</div>

Styling With CSS

To ensure the </div> elements in the board are arranged in a 3×3 grid, apply the following styles:

1
.board {
2
    display: grid;
3
    grid-template-columns: repeat(3, 1fr);
4
    gap: 10px;
5
    max-width: 300px;
6
    margin: 50px auto;
7
  }

For each cell in the grid, add the following styles.

1
.cell {
2
    width: 100px;
3
    height: 100px;
4
    display: flex;
5
    align-items: center;
6
    justify-content: center;
7
    font-size: 2rem;
8
    cursor: pointer;
9
    border: 1px solid #000;
10
  }

Create the styles which will add the X and O in the board cells. When it’s player O’s turn, we will add the circle class; when it’s player X’s turn, we will add the x class.

1
.x::before {
2
    content: "X";
3
    color: blue;
4
    position: absolute;
5
  }
6
  .circle::before {
7
    content: "O";
8
    color: red;
9
    position: absolute;
10
  }

JavaScript Functionality

Let’s define some variables:

1
const X_CLASS = "x";
2
const CIRCLE_CLASS = "circle";
3
const WINNING_COMBINATIONS = [
4
[0, 1, 2],
5
[3, 4, 5],
6
[6, 7, 8],
7
[0, 3, 6],
8
[1, 4, 7],
9
[2, 5, 8],
10
[0, 4, 8],
11
[2, 4, 6],
12
];

X_CLASS defines the CSS class that will be added on player X’s turn, while CIRCLE_CLASS represents the CSS class that will be added to each cell when it is player CIRCLES’s turn. 

Each array in the winning combinations represents the indexes of a winning row either horizontally, vertically, or diagonally. 

Select the elements using the DOM (Document Object Model) 

1
const cellElements = document.querySelectorAll("[data-cell]");
2
const board = document.getElementById("board");

Start by defining the initial player turn.

Next, create a function called  starGame() which looks like this: 

1
 function startGame() {
2
    cellElements.forEach((cell) => {
3
      cell.classList.remove(X_CLASS);
4
      cell.classList.remove(CIRCLE_CLASS);
5
      cell.removeEventListener("click", handleClick);
6
      cell.addEventListener("click", handleClick, { once: true });
7
    });
8
  
9
  
10
 startGame();

This function will reset the game by removing any X or O from the cells, hence clearing the board.

1
cell.classList.remove(X_CLASS);
2
cell.classList.remove(CIRCLE_CLASS);

It also removes any existing click events to ensure that there are no event handlers in any cell when the game starts. The function will also ensure that once the game starts, each cell can only be clicked once using the { once: true } option which will be responsible for toggling the players and adding O’s and X’s on the board.

Next, create a function called handleClick(), which will handle the logic of what happens when a player clicks a cell.

1
function handleClick(e) {
2
    const cell = e.target;
3
    const currentClass = circleTurn ? CIRCLE_CLASS : X_CLASS;
4
    cell.classList.add(currentClass);
5
    circleTurn = !circleTurn;
6
}

In the handleClick function, because each cell element has been assigned the handleClick function through event listeners, e.target refers to the specific cell element that was clicked.

  • const currentClass=circleTurn?CIRCLE_CLASS:X_CLASS; this class will determine whose turn it is. If circleTurn is true, the currentClass will be assigned to O’s turn; otherwise, X_CLASS will be used. This will ensure that the classes will keep toggling at every instance.
  • cell.classList.add(currentClass); will add the currentClass to the cell 
  • circleTurn=!circleTurn; will switch turns between the two players.

We now need to check for the winner. Create a checkWin() function which will take in the currentClass and check from the winning combinations, if there is a match on the board.

1
 function checkWin(currentClass) {
2
    return WINNING_COMBINATIONS.some((combination) => {
3
      return combination.every((index) => {
4
        return cellElements[index].classList.contains(currentClass);
5
      });
6
    });
7
  }

Here, we use the .some() method, which checks if at least one array in the WINNING_COMBINATIONS array returns true for the condition inside every().The .every() method will check if every index in the inner array contains currentClass, meaning all elements in that array are marked by the same player. If this condition is true for any combination, the player represented by currentClass wins the game.

To check if the game is a draw, the isDraw() function checks if every cell on the board contains either the X_CLASS or CIRCLE_CLASS class. If either player marks all cells, the game is considered a draw.

1
function isDraw() {
2
    return [...cellElements].every((cell) => {
3
      return (
4
        cell.classList.contains(X_CLASS) ||
5
        cell.classList.contains(CIRCLE_CLASS)
6
      );
7
    });
8
  }

Now update the handleClick() function to show the appropriate results  if checkWin() or isDraw() functions return true.

1
function handleClick(e) {
2
    const cell = e.target;
3
    const currentClass = circleTurn ? CIRCLE_CLASS : X_CLASS;
4
    console.log(currentClass);
5

6
    cell.classList.add(currentClass);
7
    circleTurn = !circleTurn;
8

9
    if (checkWin(currentClass)) {
10
      showResult(`${currentClass.toUpperCase()} wins`);
11
    } else if (isDraw()) {
12
      showResult(`It's a Draw`);
13
    }
14
  }

Now, let’s create theshowResult() function, which will display the results.

1
const resultModal = new bootstrap.Modal(
2
    document.getElementById("resultModal"),
3
    {
4
      keyboard: false,
5
    }
6
);
7
 function showResult(message) {
8
    resultMessage.textContent = message;
9
    resultModal.show();
10

11
  }

In the code above, we initialize a Bootstrap modal instance resultModal for the modal with ID “resultModal”. We then use the showResult(message) function to update the text content of the modal’s body (resultMessage.textContent) with the message parameter and display the modal using resultModal.show().

When the game ends, the modal will be displayed as shown below:

To close the modal, add an event listener to both the close and PlayAgain buttons. When each button is clicked, the game should reset by invoking the startGame() function. The modal will also be hidden.

1
const closeButton = document.querySelector(".modal .close");
2
document.getElementById("playBtn").addEventListener("click", startGame);
3
closeButton.addEventListener("click", () => {
4
    resultModal.hide();
5
    startGame();
6
  });

The last part of this game is to add visual indicators that show whose turn it is. Add the CSS classes for these.

1
.board.hover-x [data-cell]:hover:not(.x):not(.circle) {
2
    background-color: lightblue;
3
  }
4

5
  .board.hover-circle [data-cell]:hover:not(.x):not(.circle) {
6
    background-color: lightcoral;
7
  }

At the top of the file, define these variables, which represent the hover classes.

1
const HOVER_X_CLASS = "hover-x";
2
const HOVER_CIRCLE_CLASS = "hover-circle";

Next, create a function called setBoardHoverClass(), whose purpose will be to add a hover color effect depending on whose turn it is. This will ensure that the players will know whose turn it is when they hover over the board cells. 

In the setBoardHoverClass,  we first remove any existing classes .

1
function setBoardHoverClass() {
2
  board.classList.remove(HOVER_X_CLASS);
3
  board.classList.remove(HOVER_CIRCLE_CLASS);
4
  
5
}

Then, we create an if-else statement that checks whose turn it is and applies the appropriate classes to the board cells.

1
function setBoardHoverClass() {
2
  board.classList.remove(HOVER_X_CLASS);
3
  board.classList.remove(HOVER_CIRCLE_CLASS);
4
  if (circleTurn) {
5
    board.classList.add(HOVER_CIRCLE_CLASS);
6
  } else {
7
    board.classList.add(HOVER_X_CLASS);
8
  }
9
}

Finally go back to the handleClick() function and update it as follows:

1
function handleClick(e) {
2
  const cell = e.target;
3
  const currentClass = circleTurn ? CIRCLE_CLASS : X_CLASS;
4
  console.log(currentClass);
5

6
  cell.classList.add(currentClass);
7
  circleTurn = !circleTurn;
8

9
  if (checkWin(currentClass)) {
10
    showResult(`${currentClass.toUpperCase()} wins`);
11
  } else if (isDraw()) {
12
    showResult(`It's a Draw`);
13
  } else {
14
    setBoardHoverClass();
15
  }
16
}

When you hover over any cell after the game starts, you should see the effect. 

Final Result

Let’s remind ourselves what we’ve built!

Conclusion

This tutorial has covered how to create a tic-tac-toe game in JavaScript. The concepts covered will serve as a solid foundation for building even more complex games.