Creating a Memory Card Game with HTML, CSS, and JavaScript
Adam Nagy
Posted on February 10, 2023
In this tutorial, we will learn how to build a beginner-friendly memory card game using HTML, CSS, and JavaScript. The game will have a grid of cards, where each card will have an image and a score system to keep track of the player's progress. The game will be restarted when all cards have been matched.
Video tutorial
If you would prefer to watch a beginner-friendly step-by-step video tutorial instead, here is the video tutorial that I made.
Project Set-Up
Before we jump into anything, check out my repository on GitHub for this project, where I prepared an assets folder with the fruit icons that we'll use for the cards, and also created a json file with all the card data that we'll need to implement this game.
To start, let's create a new directory for our project and set up the file structure. In your code editor, create a new folder called "memory-card-game". Inside the folder, create three files: index.html, style.css, and script.js.
HTML Structure
The HTML structure for our memory card game will consist of a main container for the cards and a restart button. Don't forget to add the link
tag for the stylesheet in the head
tag. This is a really simple HTML structure as we will create all the cards dynamically in javascript.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Memory Card Game</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>Memory Cards</h1>
<div class="grid-container">
</div>
<p>Score: <span class="score"></span></p>
<div class="actions">
<button onclick="restart()">Restart</button>
</div>
<script src="index.js"></script>
</body>
</html>
CSS Styles
In css we'll start with some general styling. The body tag should be as big as the viewport so I'll add a minimum of 100% width and height for the viewport.We'll set a dark background and a white color for the texts that appear on the page.
body {
min-height: 100vh;
min-width: 100vw;
background-color: #12181f;
color: white;
}
h1 {
text-align: center;
font-weight: 700;
font-size: 50px;
}
p {
text-align: center;
font-size: 30px;
font-weight: bold;
}
The action area will be really simple, we'll center the .actions
container using flexbox, then apply some basic nice styles to the button.
.actions {
display: flex;
justify-content: center;
}
.actions button {
padding: 8px 16px;
font-size: 30px;
border-radius: 10px;
background-color: #27ae60;
color: white;
}
For the card container I'll use a centered CSS grid with 16px of grid-gap. I'll create 6 140px wide coulmns and 3 rows. I'll calculate the height of the rows by dividing the width with two and multiply it by 3. I do this to have the perfect aspect ratio of 2 by 3 (which is the standard playing card aspect ratio) for the cards. For the cards I'll apply the same dimensions and add a little bit of border-radius. The important part is that we need to set position: relative
, so we can later position the back fo the card absolutely. I'll apply a transform style of preserve-3d
and apply a little eased transition so our flipping animations will be really smooth.
.grid-container {
display: grid;
justify-content: center;
grid-gap: 16px;
grid-template-columns: repeat(6, 140px);
grid-template-rows: repeat(2, calc(140px / 2 * 3));
}
.card {
height: calc(140px / 2 * 3);
width: 140px;
border-radius: 10px;
background-color: white;
position: relative;
transform-style: preserve-3d;
transition: all 0.5s ease-in-out;
}
For the fruit icons I'll use a fix width of 60px by 60px. Then I'll create a css rule to rotates (flips) the card by 180 degrees when the .flipped
class is applied to the card. I'll center the front face layout of the card both vertically and horizontally using flexbox. Will center everything on both the back and front using absolute positioning. It is also important to set backface-visibility: hidden;
, because otherwise the back-face content of the card would still be visible when the card is flipped.
.front-image {
width: 60px;
height: 60px;
}
.card.flipped {
transform: rotateY(180deg);
}
.front, .back {
backface-visibility: hidden;
position: absolute;
border-radius: 10px;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.card .front {
display: flex;
justify-content: center;
align-items: center;
}
For the back of the card I'll use and SVG pattern, feel free to use the one you like the most from Pattern Monster. Also center the background image with background-position
, and make it cover the full card by adding background-size: cover;
.card .back {
background-image: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='25' height='25' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='hsla(0,0%,100%,1)'/><path d='M25 30a5 5 0 110-10 5 5 0 010 10zm0-25a5 5 0 110-10 5 5 0 010 10zM0 30a5 5 0 110-10 5 5 0 010 10zM0 5A5 5 0 110-5 5 5 0 010 5zm12.5 12.5a5 5 0 110-10 5 5 0 010 10z' stroke-width='1' stroke='none' fill='hsla(174, 100%, 29%, 1)'/><path d='M0 15a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm25 0a2.5 2.5 0 110-5 2.5 2.5 0 010 5zM12.5 2.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm0 25a2.5 2.5 0 110-5 2.5 2.5 0 010 5z' stroke-width='1' stroke='none' fill='hsla(187, 100%, 42%, 1)'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
background-position: center center;
background-size: cover;
}
Javascript implementation
Let's make this game interactive! We'll start by saving a reference to our grid container which will hold the cards using querySelector
. Then we will create the global variables that we will use throughout the game. The cards variable will be an array holding all of our cards, we will have two card variables that will be used for comparison. The lockboard variable will be responsible for locking the board up while the comparison and the animations runs. Also we will keep track of the user's score and initialise it with zero.
const gridContainer = document.querySelector(".grid-container");
let cards = [];
let firstCard, secondCard;
let lockBoard = false;
let score = 0;
document.querySelector(".score").textContent = score;
We use the fetch method to get the card information from the JSON file located at "./data/cards.json". This returns a promise that we convert into JSON using "res.json()".
Finally, we create a new array of cards by spreading the JSON data twice (as we need 2 of each card to be able to find duplicates) and shuffling it with the "shuffleCards" function. And voila! The cards are generated on the page with the help of the "generateCards" function.
fetch("./data/cards.json")
.then((res) => res.json())
.then((data) => {
cards = [...data, ...data];
shuffleCards();
generateCards();
});
Now let's write the function that shuffles the card data so that the game is different every time it's played. It's an essential part of making your memory card game fun and unpredictable.
The function starts by setting the "currentIndex" to the length of the "cards" array. This allows us to shuffle all the cards in the array. Then, using a "while" loop, we swap the current card with a random card until all cards have been shuffled.
The "randomIndex" is generated using Math.floor and Math.random, which returns a random whole number between 0 and the length of the "cards" array. We store the current card value in a temporary variable, "temporaryValue", and then swap it with the random card.
This process continues until the "currentIndex" reaches 0, at which point all the cards will have been shuffled. This is called the 9Fisher-Yates algorithm. With this simple but effective function, you can ensure that your memory card game is never the same twice!
function shuffleCards() {
let currentIndex = cards.length,
randomIndex,
temporaryValue;
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = cards[currentIndex];
cards[currentIndex] = cards[randomIndex];
cards[randomIndex] = temporaryValue;
}
Let's write a function that will generate the cards for us.The function uses a for loop to iterate over the cards
array and create a div
element for each card. The cardElement
is then given the class card
and a data-name
attribute with the card's name. We will use this data attribute for the comparsion. The innerHTML
property is used to define the front and back of the card, including the card's image.
Finally, the cardElement
is appended to the gridContainer
and an event listener is added to listen for a click
event. When the card is clicked, the flipCard
function is called, allowing the player to flip the card and reveal its image.
function generateCards() {
for (let card of cards) {
const cardElement = document.createElement("div");
cardElement.classList.add("card");
cardElement.setAttribute("data-name", card.name);
cardElement.innerHTML = `
<div class="front">
<img class="front-image" src=${card.image} />
</div>
<div class="back"></div>
`;
gridContainer.appendChild(cardElement);
cardElement.addEventListener("click", flipCard);
}
}
Now we'll write the flipcard
function. The function starts by checking if the "lockBoard" variable is true. If it is, the function returns, preventing any cards from being flipped. If the player clicks on the same card twice, the function also returns, avoiding any unwanted behavior.
The card that was clicked is then given the class "flipped", which causes it to flip over and reveal its image. If this is the first card being flipped, the "firstCard" variable is set to the card that was clicked. If a second card is being flipped, the "secondCard" variable is set, and the score is increased by one. The score is displayed on the page by updating the text content of the ".score" element.
Finally, the "lockBoard" variable is set to true, preventing any additional cards from being flipped. The "checkForMatch" function is then called to see if the two flipped cards match.
function flipCard() {
if (lockBoard) return;
if (this === firstCard) return;
this.classList.add("flipped");
if (!firstCard) {
firstCard = this;
return;
}
secondCard = this;
score++;
document.querySelector(".score").textContent = score;
lockBoard = true;
checkForMatch();
}
In checkForMatch
the first line, let isMatch = firstCard.dataset.name === secondCard.dataset.name;
compares the data-name
attribute of the first and second cards to see if they match. If they do match, the isMatch
variable is set to true
.
Next, the isMatch ? disableCards() : unflipCards();
line uses a ternary operator to call either the "disableCards" or unflipCards
function, depending on the result of the comparison.
The disableCards
function removes the click
event listener from the first and second cards, effectively disabling them. The resetBoard
function is then called to reset the game for the next round.
The unflipCards
function uses the setTimeout
function to wait for 1 second before removing the flipped
class from the first and second cards. This causes the cards to flip back over, allowing the player to try again. The resetBoard
function is then called to reset the game for the next round.
function checkForMatch() {
let isMatch = firstCard.dataset.name === secondCard.dataset.name;
isMatch ? disableCards() : unflipCards();
}
function disableCards() {
firstCard.removeEventListener("click", flipCard);
secondCard.removeEventListener("click", flipCard);
resetBoard();
}
function unflipCards() {
setTimeout(() => {
firstCard.classList.remove("flipped");
secondCard.classList.remove("flipped");
resetBoard();
}, 1000);
}
This resetBoard function is called to reset the state of the game after each match attempt. It sets the values of firstCard, secondCard, and lockBoard back to their default state, allowing for a new round to begin.
function resetBoard() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
The restart function is used to start the game from scratch. It calls the resetBoard function, shuffles the cards, resets the score back to 0, clears the grid container and generates new cards for the game. This function makes it easy to play the game multiple times without having to refresh the page.
function restart() {
resetBoard();
shuffleCards();
score = 0;
document.querySelector(".score").textContent = score;
gridContainer.innerHTML = "";
generateCards();
}
That is all you need to create a memory card game! Feel free to implement new features to it, like a scoreboard, and make it responsive!
Where can you learn more from me?
I create education content covering web-development on several platforms, feel free to π check them out.
I also create a newsletter where I share the week's or 2 week's educational content that I created. No bullπ© just educational content.
π Links:
- πΊ Support free education and buy me a beer
- π¬ Join our community on Discord
- π§ Newsletter Subscribe here
- π₯ YouTube Javascript Academy
- π¦ Twitter: @dev_adamnagy
- π· Instagram @javascriptacademy
Posted on February 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024