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>
- Game Board (
<div id="board">
): This div acts as a container for the nine game cells, which will be dynamically added via JavaScript. - Player Turn Indicator (
<p id="player-turn">
): This paragraph element displays the current player's turn. - The JavaScript file (
index.js
) will handle the game logic and interactions. Thescript
tag is placed within the<body>
element (at the bottom), ensuring that any HTML elements referenced by the JavaScript code are already defined and accessible when the script runs.
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;
}
-
Grid Layout: The
display: grid
property combined withgrid-template-columns: repeat(3, 1fr)
creates a structured 3x3 layout. Therepeat(3, 1fr)
value specifies a repeating pattern of three columns each with equal width (1fr
, which stands for "fraction of the available space"), allowing us to create a grid with multiple rows and columns. -
Borders and Sizing: The cells have borders on the left and bottom, while the board itself has a top and right border to complete the grid. The
box-sizing: border-box;
property ensures that borders are included in the element’s total width and height, preventing the need to adjust the board’s size manually (e.g., setting it to 302px instead of 300px). -
Cell Styling: The cells use
display: flex
to center content both vertically and horizontally.
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);
}
- Board Cell Creation: Nine
<div>
elements are created dynamically, representing the game cells. These are stored in theboardCells
array for easy access. - Event Handling: Each cell is assigned a click event listener that triggers the
handleCellClick
function to manage player turns.
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';