JavaScript Notes
Build a complete Tic-Tac-Toe game from scratch using vanilla JavaScript. Learn DOM manipulation, game state management, win detection algorithms, and event handling through this hands-on project tutorial.
Introduction
Tic-Tac-Toe is one of the best beginner-to-intermediate JavaScript projects because it combines DOM manipulation, event handling, state management, and algorithmic thinking into a single, focused application.
In this tutorial, you will build a fully functional two-player Tic-Tac-Toe game from scratch using vanilla JavaScript — no libraries, no frameworks. By the end, you'll understand how to:
- Manage game state with a simple array
- Detect wins using combination checking
- Handle user interactions with click events
- Dynamically update the DOM based on state changes
- Implement reset and draw detection logic
This project is perfect for anyone who has learned JavaScript basics and wants to apply that knowledge to a real interactive application. This tutorial is useful for all learners — concepts are explained in simple language.
Step 1 — HTML Structure
We need a container with 9 cells arranged in a 3×3 grid, a status display showing whose turn it is, and a reset button.
<!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>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="game-container">
<h1>Tic-Tac-Toe</h1>
<p id="status">Player X's Turn</p>
<div class="board" id="board">
<div class="cell" data-index="0"></div>
<div class="cell" data-index="1"></div>
<div class="cell" data-index="2"></div>
<div class="cell" data-index="3"></div>
<div class="cell" data-index="4"></div>
<div class="cell" data-index="5"></div>
<div class="cell" data-index="6"></div>
<div class="cell" data-index="7"></div>
<div class="cell" data-index="8"></div>
</div>
<button id="resetBtn">Reset Game</button>
</div>
<script src="script.js"></script>
</body>
</html>Each cell has a data-index attribute — this links the DOM element to a position in our state array.
Step 2 — CSS Styling
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #1a1a2e;
color: #eee;
}
.game-container {
text-align: center;
}
.board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 5px;
margin: 20px auto;
justify-content: center;
}
.cell {
background: #16213e;
border: 2px solid #0f3460;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2.5rem;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.cell:hover {
background: #1a1a4e;
}
.cell.x { color: #e94560; }
.cell.o { color: #53d8fb; }
.cell.winner {
background: #0f3460;
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
#status {
font-size: 1.3rem;
margin: 15px 0;
min-height: 30px;
}
#resetBtn {
margin-top: 15px;
padding: 10px 30px;
font-size: 1rem;
background: #e94560;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
#resetBtn:hover {
background: #c0392b;
}CSS Grid is perfect for a 3×3 board — grid-template-columns: repeat(3, 100px) creates three equal columns without needing complex layout calculations.
Step 3 — Game State Variables
Why an array for the board? The array board holds 9 positions (indices 0–8). Each position is either '' (empty), 'X', or 'O'. This flat array maps directly to the 3×3 grid — index 0 is top-left, index 8 is bottom-right.
Why define win conditions as an array of arrays? Rather than writing complex if-else logic, we store every possible winning line. To check for a win, we simply loop through these 8 combinations and test if all three positions share the same mark.
Step 4 — Handling Cell Clicks
Notice the separation of concerns: we first update the board array (state), then update the DOM (view). The guard clause pattern prevents invalid moves — if a cell already has a mark or the game has ended, we return early.
Step 5 — Win Detection Algorithm
How the algorithm works:
- We use destructuring
const [a, b, c] = winConditions[i]to get the three indices of each winning line. - If any position in a combo is empty, we skip it with
continue. - If all three positions share the same value, that player wins.
- If no winner is found and no empty cells remain (
!board.includes('')), it's a draw. - Otherwise, we toggle
currentPlayerusing the ternary operator.
Step 6 — Highlighting the Winning Line
This function adds a CSS class to the three winning cells, triggering the pulse animation defined in our stylesheet. The forEach loop iterates over the winning indices and applies the class to each corresponding DOM element.
Step 7 — Reset Functionality
Reset must synchronize both state and DOM. We clear every position in the board array, reset the current player to X, re-enable the game, update the status message, and remove all classes and text content from every cell.
Step 8 — Attaching Event Listeners
We use forEach to attach event listeners to all 9 cells. Each cell triggers the same handleCellClick function — the function identifies which cell was clicked via event.target and the data-index attribute.
Complete JavaScript Code
Key Concepts Reinforced
| Concept | How It's Used |
|---|---|
| Array as state | board array holds game state separate from DOM |
| DOM manipulation | textContent, classList.add/remove update the view |
| Event delegation | Click events on cells trigger shared handler |
| Destructuring | const [a, b, c] = winConditions[i] |
| Guard clauses | Early return prevents invalid moves |
| Ternary operator | currentPlayer === 'X' ? 'O' : 'X' toggles player |
| Template literals | Status messages use backtick strings |
data-* attributes | Link DOM elements to array indices |
includes() | Check for draw by testing empty cells |
Enhancement Ideas
Once you have the base game working, try adding these features:
- Score tracking — maintain a wins counter for each player using an object
{ X: 0, O: 0, draws: 0 } - AI opponent — implement a simple computer player that picks a random empty cell, or use the minimax algorithm for an unbeatable AI
- Keyboard support — let players use number keys 1–9 to place marks
- Animations — add CSS transitions for placing marks and smooth resets
- LocalStorage persistence — save the score across browser sessions
Common Mistakes to Avoid
- Using the DOM as state — never read cell.textContent to determine game logic. Always read from the
boardarray. - Forgetting to disable clicks after game ends — without the
gameActiveflag, players can keep clicking after a win. - Not resetting classes on reset — if you only clear textContent but leave classes, colors remain from the previous round.
- Using
==instead of===— always use strict equality to avoid unexpected type coercion.
Summary
This project teaches you how to think in terms of state → logic → render. The board array is the single source of truth, the check logic determines outcomes, and the DOM rendering simply reflects the current state. This same pattern scales to much larger applications — frameworks like React formalize this exact approach.
You practiced DOM selection (querySelectorAll, getElementById), event handling (addEventListener), array methods (forEach, includes, map, filter), destructuring, template literals, and conditional logic — all core JavaScript skills that appear in interviews and real-world codebases.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for JavaScript Tic-Tac-Toe Game — DOM Manipulation Project Tutorial 2026.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this JavaScript Master Course topic.
Search Terms
javascript-complete-guide, javascript master course, javascript, complete, guide, mini, projects, tic
Related JavaScript Master Course Topics