The World’s Most Empowering Tic-Tac-Toe JavaScript Tutorial

The World’s Most Empowering Tic-Tac-Toe JavaScript Tutorial

Tic-Tac-Toe is a great starter project for any new programmer!

Image for post

This tutorial is meant for absolute beginners. I?ve included links to documentation. Please look it over. This tutorial is 100% vanilla JavaScript with minimal CSS and HTML. Take your time and let me know how it works out.

EDIT: If you get really stuck, here?s the completed code:

You will need:

  • VS Code
  • The internet (and since you?re reading an online tutorial, you?re halfway there! Way to go!)

Getting Started

Start by creating a directory in your computer titled, Cheryl?s Tic-Tac-Toe and create these child directories: js, css; and these three files: index.html, css/style.css, and js/main.js.

Review the basics:

Always start any programming task by clarifying what you want to do and then breaking it down into small steps. Small steps can get you just about anywhere if you?ve got enough time. If you get stuck, break it down smaller, girl!

Tic-Tac-Toe is a two-player game played on a 3 x 3 grid. Players take turns. One marks ?X? and the other ?O?. A winner is declared when one player can align 3 of their markings in a row, in a column, or diagonally. In the event that there are no longer available spaces and no one has won, a tie is declared. Broken down, this is our game plan:

  • 3 x 3 grid
  • Make Your Mark
  • Take Turns
  • ?X? and ?O?
  • Win Logic: align 3 in a row, in a column, or diagonally
  • Declare a Tie
  • The Game is Finished

3 x 3 Grid

Ahh, the foundation of our game!

Make an HTML boilerplate by typing ! + enter if you?re in VS Code (which you are because I told you to be and we can?t do this tutorial if we can?t trust each other, now can we?)

Link up your CSS and JavaScript by including them in the head of the HTML:

<head><meta charset=”UTF-8″><meta name=”viewport” content=”width=device-width, initial-scale=1.0″><meta http-equiv=”X-UA-Compatible” content=”ie=edge”><!– this is how we link up the stylesheet –><link href=”css/style.css” rel=”stylesheet” type=”text/css”><!– And this is how we link up the JS! –><script defer src=”js/main.js”></script><!– Be sure to give your game a fun title –><title>Empowering Tic Tac Toe for the Good of Humanity</title></head>

A note on that defer in the <script> tag: the word defer will tell the browser to wait to execute that JS block until the DOM is fully parsed. This way, it can safely be included at the top of our HTML document.

I am Your Console AMA: Test that your main.js is linked up by writing this line of code in main.js:

console.log(‘Your JS is linked up. Be the person you needed when you were little.’)

If you open a live server of the document and check your dev tools, you should see your message in the console.

Back in the <body> of index.html:

<h2>Tic-Tac-Toe</h2><h2>It’s X’s turn!</h2><!– Many websites are just divs on divs on divs. Pay attention to the class and id attributes. We will be using those later to style and find elements in the document –><div class=”flex-container flex-column”><div class=”flex-container flex-wrap” id=”board”><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div><div class=”square”></div></div><!– A reset button because users are going to want to play round after round of your glorious game without ever refreshing the browser! –><button id=”reset-button”>reset</button></div>

Many programmers are loathe to write CSS. Many would have us believe that?s because they are serious programmers who don?t want to waste time on CSS fluff, however, my dear Cheryl or Larry or whoever you may be, YOU are secure in your abilities and you know that presentation is a big part of the experience of your product. So, I?m going to provide some basic styling, but you aren?t going to be afraid to make it your own! Go on! Make it shine, Cheryl!

in css/style.css:

body {text-align: center;font-family: Arial, Helvetica, sans-serif;}h2 {margin: 0 auto;font-size: 35px;margin: 10px;}.flex-container {/* In order to use flex-box we need to first set our display property */display: flex;/* center the container horizontally */justify-content: center;/* this will center content vertically */align-items: center;/* tells the container to align the children in columns rather than rows */}.flex-column {height: 100%;width: 100%;flex-direction: column;}.flex-wrap {/* tells the container to drop down once it reaches max-width */flex-wrap: wrap;height: 432px;width: 432px;}.square {border: 2px solid rgba(0, 0, 0, .75);height: 140px;width: 140px;}#reset-button {text-align: center;font-size: 20px;border: 1px solid black;height: 55px;width: 100px;margin: 10px;}

If you haven?t worked with flex-box, know that it is a powerful tool for layout. Another great option is the CSS grid. Do not fear them, be fierce instead and read up over here.

Make Your Mark

We?ve got a resplendent 3 x 3 gameboard, our CSS and JS are linked up, let?s make our mark, shall we?

Organize your js/main.js with these headings:

/*—– constants —–*//*—– app’s state (variables) —–*//*—– cached element references —–*//*—– event listeners —–*//*—– functions —–*/

This bit of organization will help keep our code clean and readable. It will also help us organize our code so we can write more efficiently. Slay on, Cheryl, slay on.

What does it mean to make a mark? Making a mark means that there is a place for the mark to be made:

/*—– app’s state (variables) —–*/let board;

Making a mark means that a game has been initiated.

Let?s use a function to initiate the board as an array of 9 empty strings. Our JS doesn?t know anything about the HTML document yet. Each position in the JS array will correspond to a square on the HTML board. Let?s give ourselves a visual aid by mimicking the 3 x 3 grid from our HTML. The 3 x 3 structure of the array changes nothing. It could be written on one single line!

function init() {board = [”, ”, ”,”, ”, ”,”, ”, ”];};//be sure to call the init function!init();

Making a mark means rendering a change in the game.

Let?s stub up a render function:

function render() {}

Now, let?s think about what we want to do. This render function will need to iterate over our board array and ?inject? the mark into the correct div. Let?s use a handy little function called .forEach(). This function accepts a callback function that it will call on each element in the array:

function render() {board.forEach(function(mark, index){console.log(mark, index);});};

Call render():

function init() {board = [”, ”, ”,”, ”, ”,”, ”, ”];// new code hererender();};

Go ahead and open a server then hit cmd + opt + i to check out your console! You should see all the index numbers from the board! Try throwing something into the empty strings on the board array and see what happens in the console.

Making a mark means that the mark shows up on the page.

Grab our squares:

/*—– cached element references —–*/const squares = Array.from(document.querySelectorAll(‘#board div’));

The Array.from() function will make an array from all elements returned by querySelectorAll. Notice that querySelectorAll is finding the element with the id of .board and selecting all the div children of that element. This way, we didn?t have to give each square an id, select them individually, and build a new array. JavaScript did that for us! Pretty cool!

Pass those to render():

function render() {board.forEach(function(mark, index) {//this sets the text content of the square of the same position to the mark on the board. squares[index].textContent = mark;});};

And now, you have made a mark, Cheryl! Treat yourself!

Take turns

What does it mean to take turns? Taking a turn means that there is a turn.

Head back to the state section and throw down your turn variable like a boss:

/*—– app’s state (variables) —–*/let board;let turn = ‘X’;

Taking a turn means that an event (such as ?click?) has occurred.

Listen for the click on the board. We will need to ?grab? an HTML element using getElementById() and then chain the event listener onto it:

/*—– event listeners —–*/document.getElementById(‘board’).addEventListener(‘click’, handleTurn);

addEventListener() takes two arguments, the event to listen for and a callback function to execute when the event is heard. We need to write that callback function:

/*—– functions —–*/function handleTurn(event) {let idx = squares.findIndex(function(square) {return square ===;});};

The ?event? is the click, the ?target? is the element on which the event took place ? the square we?ve clicked on. findIndex() finds the index of the square in our squares array that matches the square the user clicked!

Excellent work! You can change the world, Cheryl.

Did I say world? I meant board. Change the board we?re holding in state:

function handleTurn(event) {let idx = squares.findIndex(function(square) {return square ===;});// new code belowboard[idx] = turn;// check your console logs to make sure it’s working!console.log(board);};

Has your state changed? You?re on the right track in this game and in your life. You?ve got this. You can change the board for the better!

?X? and ?O?

We still don?t have ?O? included in this game which makes it easy to win, but not much fun to play. So, we?ll just update that bit of state using a ternary.

Ternaries seem to confuse people. Learn to write one today and thank yourself for all time. Here?s our logic: If it is X?s turn, then assign the turn to O; if it is not X?s turn, assign the turn to X. Let?s put that logic into the ternary structure:

<condition> ? <if condition is true, this> : <else if condition is false, this>

Place the assignment ?turn =? in front of that ternary and try console logging it out. It?s ok if this takes a minute. Just breathe, Cheryl. Don?t forget to call that render() function once you?re all through. All together it should look like this:

function handleTurn() {let idx = squares.findIndex(function(square) {return square ===;});board[idx] = turn;// This is tidyturn = turn === ‘X’ ? ‘O’ : ‘X’;// In an if statement it would look like: // if (turn === ‘X’) {// turn = ‘O’ // } else {// turn = ‘X’ // };// writing the ternary saved you from all that. render();};

Ta-freaking-da! Now we have a Tic-Tac-Toe game! Nothing can dim the light that shines from within you!

Let?s fix our <h2> so our user gets the right message and knows when it?s their turn:

/*—– cached element references —–*/const squares = Array.from(document.querySelectorAll(‘#board div’));// new code belowconst messages = document.querySelector(‘h2’);

When should we change the message? When we render!

function render() {board.forEach(function(val, idx) {squares[idx].textContent = val;});// new code belowmessages.textContent = `It’s ${turn}’s turn!`;};

The backticks (`) allow us to use template literals to embed JS expressions in strings.

Win Logic

What does it mean to win?

Winning means there?s such as thing as winning:

/*—– app’s state (variables) —–*/let board;let turn = ‘X’;// new code belowlet win;

Every time a player takes a turn, we need to check if they made a winning move. Let?s start with our top row. It has the indexes 0 through 2. So, the winning logic will be: There is a win if there is a mark in index 0 and it matches the marks in indexes 1 and 2. Ternaries all day.

win = board[0] && board[0] === board[1] && board[0] === board[2] ? board[0] : null;

Note that we?re checking that there is something in board[0] first. If we just check that all three positions are the same, we?ll get a winner when they’re all empty. The first board[0] prevents that. This ternary has three conditions, but it?s still the same structure:

<condition1 && condition2 && condition3 > ? <if all 3 conditions are true, this> : <else, this>

Use your console to check if your win variable is working correctly by typing win and seeing if your console returns true or false. You could write out all those statements and it would work for your win logic, OR?


We have 3 (rows) + 3 (columns) + 2 (diagonals) = 8 possible ways to win. Let?s make an array of the indexes of winning combinations. This is where you may find the 3 x 3 shape of our board array helpful for finding the index numbers of winning combos. Make an array of combos where each element is a nested array of the index numbers of winning moves:

/*—– constants —–*/const winningCombos = [[0, 1, 2],[3, 4, 5],[6, 7, 8],[0, 3, 6], [1, 4, 7],[2, 5, 8],[0, 4, 8],[2, 4, 6]];

Create a new function that will test for the winner every turn:

/*—– functions —–*/function getWinner() {// just stub it up for now.}

Now, let?s refactor our handleTurn() function to call getWinner() and assign its output to the win variable:

function handleTurn() {let idx = squares.findIndex(function(square) {return square ===;});board[idx] = turn;// new code herewin = getWinner();render();};

Cheryl, you are a treasure. Now, we need the getWinner() function to iterate through that winningCombos array and check if one of the players has a winning combination of marks on the board. We can use winningCombos.forEach() to do that. That way, we only need to write the win logic one time, but it will be checked against all 8 of our possible winning combinations. Our forEach() callback function will accept two arguments, the element in the array, and the index of the element: function(combo, index) Once again, we can use the same logic, but we?ll be testing if all three marks match a combo in the winning combos array. Give it a shot! You will end up with something like this:

function getWinner() {let winner = null;winningCombos.forEach(function(combo, index) { if (board[combo[0]] && board[combo[0]] === board[combo[1]] && board[combo[0]] === board[combo[2]]) winner = board[combo[0]]; }); return winner;};

That looks like our logic from before, but we?ve written a function to check all the winning combos at once. Use the console.log() to check your function.

Werk! Let?s update our message so it shows the winner when there is a winner. This can be done in a single line using our friend, the ternary.

messages.textContent = win ? `${win} wins the game!` : `It’s ${turn}’s turn!`;

Declare a Tie

What does it mean to tie? It means the board is full and there are no remaining moves or opportunities to win. Let?s put that into the getWinner() function:

function getWinner() {let winner = null;winningCombos.forEach((combo, index) => {if (board[combo[0]] && board[combo[0]] === board[combo[1]] && board[combo[0]] === board[combo[2]]) {winner = board[combo[0]];}});// new code belowreturn winner ? winner : board.includes(”) ? null : ‘T’;};

Or, if you want to see without the ternary:

function getWinner() {let winner = null;winningCombos.forEach((combo, index) => {if (board[combo[0]] && board[combo[0]] === board[combo[1]] && board[combo[0]] === board[combo[2]]) {winner = board[combo[0]];}});// new code belowif (winner) { return winner } else if (board.includes(”)) { return null // if there’s an empty space, return null (no winner yet)} else { return ‘T’ // no winner and no empty spaces? That’s a tie!}

Now, let?s update the message so it won?t display ?T wins the game!? if there is a tie. Try having a look at this nested ternary and translating it into plain-spoken language:

messages.textContent = win === ‘T’ ? `That’s a tie, queen!` : win ? `${win} wins the game!` : `It’s ${turn}’s turn!`;

The text content of the messages is going to be: if win is ?T? then, ?That?s a tie, queen!?, if there is a win then, ?<winner> wins the game!?, else, ?It?s <turn>?s turn.? It may be confusing at first glance, but that doesn?t make you any less of a badass. Go over it again or try writing out the equivalent in if?else statements. If you get stuck, don?t sweat it! I have written the solution below.

Classic if/else statement version:

if ( win === ‘T’ ) { messages.textContent = `That’s a tie, queen!`} else if (win) { messages.textContent = `${win} wins the game!`} else { messages.textContent = `It’s ${turn}’s turn!`}

Ternaries are fun, but a word of caution: not everyone likes seeing them nested as it can diminish readability, but I don?t want you to be confused when you come across them.

The Game is Finished

The game is finished. Let?s get that reset button working so we can get a fresh start:

/*—– event listeners —–*/document.getElementById(‘board’).addEventListener(‘click’, handleMove);//new codedocument.getElementById(‘reset-button’).addEventListener(‘click’, init);

I just re-used the init() function to reset the game, but you don?t have to. This is your world, Cheryl.

Go back over the working game and try challenging yourself to look for ways to improve it. Here are some ideas:

  • Make it so players can keep score
  • Make it so players have the option of who can go first (right now, X always goes first)
  • Add some more badass styling
  • Make it so after a square is filled in with one mark it cannot be changed
  • See if you can re-write a function a different way
  • Make it responsive for users on mobile
  • Come up with your own improvement and build it out!

Slay on, Cheryl, slay on.

No Responses

Write a response