How to create a memory game with vanilla JavaScript

Finished Game

Let’s begin by taking a look at what we’re building:

Create a Grid

The main component of the game will be a grid, which will consist of a 4×4 grid that will look like this;

When a user clicks on a card, it will be flipped to reveal the number beneath, and the timer will start. On the second click, if the new card matches the first, the card will stay open, as shown below. At any given time, only two cards will be shown.

To make it possible to flip a card, each card in the grid will have a back and front part. We will also have a </h3> element to display the timer.

1
 <div class="container">
2
 <h1 class="text-center mt-5 mb-4">JavaScript Memory Game</h1>
3
 <h3 class="text-center mb-4" id="timer">Time left: 1:00</h3>
4
 <p class="text-center mt-4">
5
 Click on the cards to reveal their numbers and find matching pairs. If
6
 you find a match, the cards stay open; otherwise, they will be hidden
7
 again. The game ends when all pairs are matched. Watch the timer to
8
 track your time. Good luck!
9
 </p>
10
11
 <div class="wrapper" id="wrapper">
12
 <div class="card">
13
 <div class="card-front"></div>
14
 <div class="card-back"></div>
15
 </div>
16
 <div class="card">
17
 <div class="card-front"></div>
18
 <div class="card-back"></div>
19
 </div>
20
 <div class="card">
21
 <div class="card-front"></div>
22
 <div class="card-back"></div>
23
 </div>
24
 <div class="card">
25
 <div class="card-front"></div>
26
 <div class="card-back"></div>
27
 </div>
28
 <div class="card">
29
 <div class="card-front"></div>
30
 <div class="card-back"></div>
31
 </div>
32
 <div class="card">
33
 <div class="card-front"></div>
34
 <div class="card-back"></div>
35
 </div>
36
 <div class="card">
37
 <div class="card-front"></div>
38
 <div class="card-back"></div>
39
 </div>
40
 <div class="card">
41
 <div class="card-front"></div>
42
 <div class="card-back"></div>
43
 </div>
44
 <div class="card">
45
 <div class="card-front"></div>
46
 <div class="card-back"></div>
47
 </div>
48
 <div class="card">
49
 <div class="card-front"></div>
50
 <div class="card-back"></div>
51
 </div>
52
 <div class="card">
53
 <div class="card-front"></div>
54
 <div class="card-back"></div>
55
 </div>
56
 <div class="card">
57
 <div class="card-front"></div>
58
 <div class="card-back"></div>
59
 </div>
60
 <div class="card">
61
 <div class="card-front"></div>
62
 <div class="card-back"></div>
63
 </div>
64
 <div class="card">
65
 <div class="card-front"></div>
66
 <div class="card-back"></div>
67
 </div>
68
 <div class="card">
69
 <div class="card-front"></div>
70
 <div class="card-back"></div>
71
 </div>
72
 <div class="card">
73
 <div class="card-front"></div>
74
 <div class="card-back"></div>
75
 </div>
76

Ensure you add Bootstrap CDN link in the head section.

1
<link
2
 href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
3
 rel="stylesheet"
4
 integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
5
 crossorigin="anonymous"
6
/>

CSS Styling

The wrapper containing the cards will have the following styles.

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

Each card will have the following styles.

1
.card {
2
 width: 100px;
3
 height: 100px;
4
 perspective: 1000px;
5
 cursor: pointer;
6
 position: relative;
7
 transform-style: preserve-3d;
8
 transition: transform 0.6s;
9
 }

The back and front parts will be positioned at an overlapping position on top of each other.

1
.card-front,
2
 .card-back {
3
 position: absolute;
4
 width: 100%;
5
 height: 100%;
6
 backface-visibility: hidden;
7
 display: flex;
8
 align-items: center;
9
 justify-content: center;
10
 border: 1px solid #000;
11
 font-size: 3rem;
12
 font-weight: 800;
13
 }

By default, any element’s back face is a mirror of the front face, so in the initial state, the front card is rotated at the its y-axis at 180 degrees, revealing the back face. When we set the property backface-visibility hidden, the back face of the font card will not be visible at the initial state.

When the card is flipped, it will rotate to show the front side of the front card.Â

The styles are important to ensure the flipping effect is achieved.

  • perspective: 1000px; ensures a 3D effect, making the rotation more realistic by simulating depth.
  • transform-style: preserve-3d; is necessary to maintain the 3D transformations for child elements, allowing both sides of the card to be manipulated in 3D space.
  • transition: transform 0.6s; set the flip effect’s duration to 0.6 seconds; This means the card will take 0.6 seconds to flip from one side to the other when the transformation is applied.

Create a Timer

We will start by working on the timer functionality. The user must match all eight pairs of numbers in one minute. Declare the following variables:

1
let timer;
2
let timeLeft = 60;

The timer variable will reference a timer interval. This will be used to start and end the timer while let timeLeft =60 sets the amount of time left for the game to 60 seconds,

Create a function called startTimer(). Inside the function, add the code below.

1
function startTimer() {
2
 timer = setInterval(() => {
3
 timeLeft--;
4
 const minutes = Math.floor(timeLeft / 60);
5
 const seconds = timeLeft % 60;
6
 timerElement.textContent = `Time left:${minutes}:${
7
 seconds < 10 ? "0" : ""
8
 }${seconds}`;
9
10
 if(timeLeft <= 0) {
11
 clearInterval(timer);
12
13
 }
14
 }, 1000);
15
}

In the code above, timer = setInterval(() => { ... }, 1000); will  decrement the time left every second and calculate and update the display to show the remaining time in minutes and seconds.

When the time left reaches 0, the timer will be cleared.

Shuffle and add numbers to cards

The next step is to add the numbers on each card. Start by selecting all the elements with the class card using the querySelectorAll method and create an array of numbers from 1-8 in duplicates.

1
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8];
2
const cards = document.querySelectorAll(".card");

The next step is to shuffle the numbers and assign a number to the front of each card. This shuffling happens every time the game is reloaded, ensuring that the numbers are in different positions each time you play.

1
const shuffledNumbers = [...numbers].sort(() => 0.5 - Math.random());
2
3
cards.forEach((card, index) => {
4
 card.querySelector(".card-front").textContent =
5
 shuffledNumbers[index];
6
7
 });

Start timer and flip cards when clicked

At this stage, our game looks like this:

When the property transform: rotateY(180deg); is applied, both cards will be rotated. The back face of the back-card will be hidden since it has the property backface-visibility: hidden; .

The front-card will also be rotated, but since the initial position was showing its backface (with backface-visibility: hidden;) , the front will now be shown, revealing the number assigned to the card to the user.

Define the following variables:

1
let timerStarted = false;
2
let clickedCards = [];
3
let matchedCards = 0;
  • The timeStarted variable will be used to check if the timer has started or not.
  • The clickedCard  array will keep track of the cards that have been clicked.Â
  • matchedCards will set up a counter to hold the number of matched cards.

Add click event listeners to each card element.Â

1
cards.forEach((card) => {
2
 card.addEventListener("click", () => {
3
 if(!timerStarted) {
4
 startTimer();
5
 timerStarted = true;
6
 }
7
8
 card.classList.add("flipped");
9
 clickedCards.push(card);
10
 }

The first time a card is clicked, the countdown begins.When any card is clicked, it’s flipped by rotating 180 degrees around the Y-axis and added to the list of clicked cards. Â

We also ensure that only two cards are in the clickedCards array at any given time. Then, we check if the numbers on the front of the two cards are the same. If they match, the following will happen:

  • The clickedCards array will be set to empty.
  • We will increment the count of the matched cards by two
  • If all cards are matched, we will stop the timer and call the endGame() function, ultimately ending the game.

If the two cards don’t match:

  • Remove the flipped class from both cards after a 1-second delay, which rotates them back to their original position.
  • Reset the clickedCards array to empty.

End game

When the game ends, we want to show an overlay over the game, which will display a message indicating whether the player has won or lost the game. We will also display a button to reset the game. The overlay will have the following HTML .

1
<div
2
 class="d-flex flex-column justify-content-center align-items-center position-absolute w-100 h-100"
3
 id="gameOverContainer"
4
 >
5
 <h2 class="gameOver"></h2>
6
 <button class="btn btn-primary mt-4" id="restart">Play Again</button>
7
 </div>

And it has the following styles

1
 #gameOverContainer {
2
 visibility: hidden;
3
 opacity: 0;
4
 background-color: rgba(0, 0, 0, 0.5);
5
 top: 0;
6
 left: 0;
7
 }
8
 #gameOverContainer.show {
9
 visibility: visible;
10
 opacity: 1;
11
 transition: opacity 0.7s linear;
12
 }

Create a function called endGame() and add the code below:

1
function endGame(isWin) {
2
3
 gameOverMessage.textContent = isWin
4
 ? "Congratulations! You won!"
5
 : "You have failed!";
6
 gameOverMessage.style.color = isWin ? "#FFD700" : "red";
7
 gameOverContainer.classList.add("show");
8
 }

When the game ends with a win, the message  “Congratulations! You won!” will be displayed in green and when a game is lost, the message “You have failed!” will be displayed in red .

By default, the overlay container will be hidden; once the game ends, it will be shown; for example, if the case of a win, it will look like this:

In the startTimer() function, update the code to call the endGame() function when the time left reaches 0.

1
function startTimer() {
2
 timer = setInterval(() => {
3
 timeLeft--;
4
 const minutes = Math.floor(timeLeft / 60);
5
 const seconds = timeLeft % 60;
6
 timerElement.textContent = `Time left:${minutes}:${
7
 seconds < 10 ? "0" : ""
8
 }${seconds}`;
9
10
 if(timeLeft <= 0) {
11
 clearInterval(timer);
12
 endGame(false);
13
 }
14
 }, 1000);
15
 }

Restart game

The next step is to reset the game when the Play Again button is clicked. Let’s start by getting the button and adding an event listener to the button.Â

1
const restart = document.getElementById("restart");
2
restart.addEventListener("click", restartGame);

The restartGame() function will be called when the button is clicked.

1
function restartGame() {
2
 gameOverContainer.classList.remove("show");
3
4
 timeLeft = 60;
5
 timerElement.textContent = "Time left: 1:00";
6
 clearInterval(timer);
7
 timerStarted = false;
8
9
 clickedCards = [];
10
 matchedCards = 0;
11
}

In the restartGame() function above:

  • The time left will be reset to 60 seconds.
  • The overlay will be hidden.
  • The timer will be reset.
  • The clickedCards array will be reset to an empty array.
  • The matched cards count will be reset to 0.

Finished product

We’ve done it! Here’s a reminder of the finished project:

Conclusion

This tutorial taught you how to create a memory game with JavaScript. We have used many concepts like DOM manipulation, array manipulation, timers, etc. A few ways to enhance the game would be to allow players to choose different themes, such as emojis, fruits, animals, etc. You could also implement the ability to choose grid sizes.