Build an Interactive Memory Card Game
Build a browser-based memory card game with HTML, CSS, and JavaScript.
Introduction
30 Second Summary
You have probably played a matching game before. Flip two cards, try to remember where each pair is hiding, and test your memory until every match is found.
In this project, you will build a fully playable memory card game from scratch using HTML, CSS, and JavaScript. You will then deploy it as a live website that anyone with the link can play.
What You'll Build
An interactive 4x4 memory card game with smooth flip animations, pair-matching logic, a move counter, and a win condition, built in StackBlitz and deployed live using Netlify.
By the end of this project, you'll have:
- A working memory card game with smooth CSS flip animations and pair-matching logic.
- Practical experience writing HTML, CSS, and JavaScript from scratch in a professional browser-based code editor.
- A live website deployed to the internet with a shareable URL.
- Secret Mission: Add a countdown timer and a restart button that reshuffles the cards.
Do I need to install any software?
This entire project runs in your browser. You will use StackBlitz as your code editor and Netlify Drop for deployment, both free and requiring no downloads.
Before We Start
Before We Start...
Set Up Your Browser-Based Dev Environment
Every game starts with a workspace. Instead of installing software on your computer, you will build this entire project in a browser-based code editor called StackBlitz.
StackBlitz gives you a professional editor with a live preview that updates as you type. You will also create a Netlify account so your finished game can go live on the internet later.
In this step, get ready to:
- Sign into StackBlitz and start a new Static project.
- Create a Netlify account for deployment.
- Verify the live preview panel works.
Create a StackBlitz account and start a Static project
StackBlitz is a browser-based code editor. It works like a desktop editor but runs entirely in your browser tab. No downloads or installations needed.
- Open a new browser tab and go to https://stackblitz.com.
- Click Sign in with GitHub on the homepage.
- Authorize StackBlitz to access your GitHub account when prompted.
What is StackBlitz?
StackBlitz is a free online development environment that runs in your browser. It gives you a code editor on the left and a live preview of your project on the right.
Every time you make a change to your code, the preview updates automatically. No need to refresh the page manually.
Now you will create a new project using the Static template.
- On your StackBlitz dashboard, click the New Project button.
- A modal appears with starter templates. Find and select Static under the HTML/JS/CSS section.
StackBlitz creates a new project with three files: index.html, styles.css, and script.js. You should see these listed in the file explorer on the left side.
The template comes with some starter code already in these files. You need to clear it out so you can start from scratch.
- Click on index.html in the file explorer to open it in the editor.
- Select all the content with Ctrl+A and press Delete to clear it.
- Click on styles.css and clear all content the same way.
- Click on script.js and clear all content the same way.
Why start with empty files?
The Static template includes sample code to demonstrate how StackBlitz works. You are clearing it so you can write every line yourself from scratch. This way you will understand exactly what each piece of code does.
Don't see the Static template?
Make sure you are logged in. If you are not logged in, you will see starter projects listed on the homepage instead of a dashboard with a New Project button. Click Sign in with GitHub first, then try again.
Help me find the Static template in StackBlitz.
Create a Netlify account for deployment later
Netlify is a hosting service that puts websites on the internet. You will use it in a later step to make your game playable by anyone with the link.
- Open a new browser tab and go to https://app.netlify.com/signup.
- Click Sign up with GitHub.
- Authorize Netlify to access your GitHub account when prompted.
- Complete the signup process by following any on-screen prompts.
Once you see the Netlify dashboard, you are all set. You will come back to Netlify in Step 5.
Getting a verification error?
Netlify sometimes flags new accounts for additional verification. Check your email inbox (including spam) for a verification message from Netlify. Click the link in that email to confirm your account.
Help me troubleshoot my Netlify signup.
Verify your setup
Time to confirm that StackBlitz is working correctly. You will type a quick test to check that the live preview updates in real time.
- Switch back to your StackBlitz browser tab.
- Click on index.html in the file explorer to open it.
- Type the following test code into the empty file:
<h1>Hello World</h1>
- Look at the preview panel on the right side of the screen.
- You should see the text "Hello World" appear in large bold letters.
What should I see?
The preview panel on the right side of StackBlitz acts like a web browser. It shows a live version of your project that updates every time you type. If you see "Hello World" rendered in the preview, your environment is working correctly.
Now that you have confirmed the preview works, clear the test code so you start the next step with a blank slate.
- Select all the text in index.html with Ctrl+A and press Delete.
- Confirm the preview panel is now empty (blank white or blank dark page).
Preview not updating?
If the preview panel is not visible, look for a panel or tab labeled Preview on the right side of the editor. StackBlitz shows a split view with code on the left and preview on the right by default.
If you still do not see any changes, try refreshing the browser tab containing StackBlitz.
Help me get the StackBlitz preview working.
Your workspace is ready. Next up, you will build the game board using HTML to create the structure of your memory card game.
Build the Game Board HTML Structure
Your coding environment is set up and ready to go. Now it is time to build the skeleton of your memory card game.
Every web page starts with HTML, which defines the structure and content a browser displays. In this step, you will create the game board layout: a header, 16 cards arranged for matching, and a win message.
In this step, get ready to:
- Create the HTML document skeleton with a header and move counter.
- Build 16 card elements (8 matching pairs) inside a game board container.
- Add a hidden win message that appears when the player completes the game.
Create the HTML skeleton with game elements
The HTML document needs a basic structure that tells the browser what to render. You will start with the page head (title and stylesheet link) and a header section showing the game title and move counter.
- In StackBlitz, click on index.html in the file list on the left to open it in the editor.
- Paste the following code into the empty file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Card Game</title>
<!-- Link to our stylesheet -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Header with game title and move counter -->
<div class="header">
<h1>Memory Card Game</h1>
<p class="stats">Moves: <span id="move-counter">0</span></p>
</div>
<!-- JavaScript file loaded at end of body -->
<script src="script.js"></script>
</body>
</html>
What does this code do?
This creates a complete but minimal HTML page.
- The <head> section sets the page title and links to styles.css so styles will apply once you write them.
- The <div class="header"> contains the game title and a move counter that starts at 0.
- The <span id="move-counter"> gives JavaScript a target to update the move count later.
- The <script> tag loads script.js at the bottom so the HTML loads first before any code runs.
- Check the live preview panel on the right. You should see "Memory Card Game" as a heading and "Moves: 0" below it.
Why does the preview update automatically?
StackBlitz detects your edits and refreshes the preview after you stop typing for about one second. You do not need to manually save or refresh.
Don't see anything in the preview?
Make sure you pasted the code into index.html (not styles.css or script.js). Check that the file tab at the top of the editor says index.html.
If the preview is blank, wait a couple of seconds for StackBlitz to refresh. You can also click the refresh arrow in the preview panel's address bar.
Help me debug my blank StackBlitz preview.
Create the card HTML structure
The game board contains 16 cards arranged as 8 matching pairs. Each card needs a layered structure: an outer container, an inner wrapper for the flip effect, a front face (showing a question mark), and a back face (showing the hidden symbol).
- In index.html, find the closing </div> of the header section (the one right after the </p> tag).
- Place your cursor on the empty line between that </div> and the <script> tag, then paste the following code:
<div class="game-board" id="game-board">
<!-- Card pair 1: dog -->
<div class="card" data-symbol="🐶">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐶</div>
</div>
</div>
<div class="card" data-symbol="🐶">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐶</div>
</div>
</div>
<!-- Card pair 2: cat -->
<div class="card" data-symbol="🐱">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐱</div>
</div>
</div>
<div class="card" data-symbol="🐱">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐱</div>
</div>
</div>
<!-- Card pair 3: frog -->
<div class="card" data-symbol="🐸">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐸</div>
</div>
</div>
<div class="card" data-symbol="🐸">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐸</div>
</div>
</div>
<!-- Card pair 4: fox -->
<div class="card" data-symbol="🦊">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦊</div>
</div>
</div>
<div class="card" data-symbol="🦊">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦊</div>
</div>
</div>
<!-- Card pair 5: unicorn -->
<div class="card" data-symbol="🦄">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦄</div>
</div>
</div>
<div class="card" data-symbol="🦄">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦄</div>
</div>
</div>
<!-- Card pair 6: panda -->
<div class="card" data-symbol="🐼">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐼</div>
</div>
</div>
<div class="card" data-symbol="🐼">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐼</div>
</div>
</div>
<!-- Card pair 7: penguin -->
<div class="card" data-symbol="🐧">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐧</div>
</div>
</div>
<div class="card" data-symbol="🐧">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐧</div>
</div>
</div>
<!-- Card pair 8: butterfly -->
<div class="card" data-symbol="🦋">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦋</div>
</div>
</div>
<div class="card" data-symbol="🦋">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦋</div>
</div>
</div>
</div>
What does this card structure do?
Each card is built from nested layers that enable the 3D flip animation later.
- The outer <div class="card"> holds a data-symbol attribute storing the emoji. JavaScript will compare these values to detect matches.
- The card-inner div is the element that rotates when flipped.
- The card-front shows a question mark (the hidden state) and card-back reveals the emoji (the flipped state).
- Every symbol appears exactly twice, giving you 8 pairs across 16 cards.
- Check the preview. You should see a list of question marks and emojis displayed as plain text (they will look messy until you add CSS in the next step).
Seeing broken or missing emojis?
Make sure your file encoding is UTF-8. The first line in the <head> section should be <meta charset="UTF-8">. This tells the browser to support emoji characters.
If the preview still shows garbled text, try copying the code block again fresh.
Help me fix emoji display issues in my HTML file.
Add a win message element
The game needs a congratulations message that appears once the player finds all 8 pairs. You will add a hidden container that JavaScript reveals at the end of the game.
- In index.html, find the closing </div> of the game-board section (the last </div> before the <script> tag).
- Paste the following code between that closing </div> and the <script> tag:
<div class="win-message" id="win-message">
<p id="win-text">Congratulations! You found all pairs!</p>
</div>
What does this code do?
This div will be hidden by CSS until the player matches all 8 pairs. JavaScript will then add a class to make it visible and update the text to include the player's move count.
The id="win-message" lets JavaScript find and show the container. The id="win-text" lets JavaScript change the congratulations text dynamically.
- Check the preview. You should see "Congratulations! You found all pairs!" at the bottom of the page (it will become hidden once you add CSS).
Don't see the congratulations text?
Make sure you placed the new code after the game-board </div> but before the <script> tag. If it is inside the game-board div, it will appear mixed in with the cards instead.
Help me figure out where to place my win-message div.
✔️ Awesome, I've got everything!
Great. Make sure the preview shows the heading, move counter, a list of question marks and emojis, and the congratulations text at the bottom.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point. Compare yours against it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Card Game</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="header">
<h1>Memory Card Game</h1>
<p class="stats">Moves: <span id="move-counter">0</span></p>
</div>
<div class="game-board" id="game-board">
<div class="card" data-symbol="🐶">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐶</div>
</div>
</div>
<div class="card" data-symbol="🐶">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐶</div>
</div>
</div>
<div class="card" data-symbol="🐱">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐱</div>
</div>
</div>
<div class="card" data-symbol="🐱">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐱</div>
</div>
</div>
<div class="card" data-symbol="🐸">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐸</div>
</div>
</div>
<div class="card" data-symbol="🐸">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐸</div>
</div>
</div>
<div class="card" data-symbol="🦊">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦊</div>
</div>
</div>
<div class="card" data-symbol="🦊">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦊</div>
</div>
</div>
<div class="card" data-symbol="🦄">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦄</div>
</div>
</div>
<div class="card" data-symbol="🦄">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦄</div>
</div>
</div>
<div class="card" data-symbol="🐼">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐼</div>
</div>
</div>
<div class="card" data-symbol="🐼">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐼</div>
</div>
</div>
<div class="card" data-symbol="🐧">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐧</div>
</div>
</div>
<div class="card" data-symbol="🐧">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🐧</div>
</div>
</div>
<div class="card" data-symbol="🦋">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦋</div>
</div>
</div>
<div class="card" data-symbol="🦋">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">🦋</div>
</div>
</div>
</div>
<div class="win-message" id="win-message">
<p id="win-text">Congratulations! You found all pairs!</p>
</div>
<script src="script.js"></script>
</body>
</html>
Your HTML structure is complete. The page looks rough right now because there is no styling yet. Next up, you will transform this plain text into a polished game board with CSS Grid and smooth flip animations.
Style the Game with CSS Grid and Flip Animations
Your HTML structure is complete, but right now the preview shows a messy list of question marks and emoji.
In this step, you will transform that raw HTML into a polished game board using CSS Grid for layout and CSS 3D transforms for a smooth card flip animation.
In this step, get ready to:
- Set up a dark theme with centered layout and styled header.
- Create a 4-column CSS Grid for the game board.
- Build a 3D card flip animation using CSS transforms.
Set up base styles and the game board grid
Every web page needs a CSS reset to remove default browser spacing, plus base styles that set the overall look and feel.
- In StackBlitz, click on styles.css in the file explorer on the left.
- Add the page reset, body layout, and header styles by pasting this code:
/* Remove default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Dark themed page with centered content */
body {
font-family: system-ui, sans-serif;
background-color: #1a1a2e;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
/* Header and title styles */
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2rem;
margin-bottom: 10px;
color: #e94560;
}
.stats {
font-size: 1.2rem;
color: #a0a0b0;
}
What does this code do?
The * selector resets all elements to have zero margin and padding, giving you a clean slate.
The body uses flexbox (display: flex) to center everything vertically and horizontally on a dark #1a1a2e background.
Check the preview panel. You should see the title "Memory Card Game" in pink text on a dark background, with all the question marks and emoji listed below it.
Now you need to arrange those 16 cards into a neat 4x4 grid. CSS Grid makes this simple with just a few properties.
- Add the game board grid styles below your existing code in styles.css:
/* 4-column grid layout for the 16 cards */
.game-board {
display: grid;
grid-template-columns: repeat(4, 100px);
gap: 12px;
max-width: 460px;
}
What does this code do?
The grid-template-columns: repeat(4, 100px) property creates exactly 4 columns, each 100 pixels wide. The 16 cards automatically flow into a 4x4 grid without you needing to position each one.
The gap: 12px adds even spacing between every card.
Check the preview. You should see the cards arranged in a 4x4 grid, though they still look like plain text right now.
Cards still in a single column?
Make sure the .game-board selector matches the class name game-board in your HTML exactly (no typos, no capital letters).
Also confirm you have display: grid on the .game-board rule. Without it, the grid columns have no effect.
Why aren't my cards showing as a grid?
Style the cards with 3D flip capability
The card flip effect works by layering two faces (front and back) on top of each other, then rotating the whole thing 180 degrees in 3D space when clicked.
- Add the card container and 3D flip mechanism below your game board styles:
/* Each card container with 3D perspective */
.card {
width: 100px;
height: 100px;
perspective: 600px;
cursor: pointer;
}
/* Inner element that actually rotates */
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s ease;
}
/* When flipped class is added, rotate 180 degrees */
.card.flipped .card-inner {
transform: rotateY(180deg);
}
How does the 3D flip work?
The perspective: 600px property gives depth to the card so rotations look 3D instead of flat. Think of it like the distance between your eyes and the card.
The transform-style: preserve-3d tells the browser that child elements (front and back faces) live in the same 3D space. The transition: transform 0.5s ease makes the rotation animate smoothly over half a second.
Now you need to style the two card faces so only one is visible at a time.
- Add the card face styles and matched state below your flip mechanism code:
/* Shared styles for both card faces */
.card-front,
.card-back {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 2rem;
}
/* Front face: dark with question mark */
.card-front {
background-color: #16213e;
border: 2px solid #0f3460;
color: #e94560;
font-weight: bold;
}
/* Back face: starts rotated so it is hidden */
.card-back {
background-color: #0f3460;
border: 2px solid #e94560;
transform: rotateY(180deg);
font-size: 2.5rem;
}
/* Matched cards stay flipped with teal highlight */
.card.matched .card-inner {
transform: rotateY(180deg);
}
.card.matched .card-back {
border-color: #4ecca3;
background-color: #1a3a4a;
}
What does backface-visibility do?
The backface-visibility: hidden property makes each face invisible when it faces away from you. Without it, you would see both faces overlapping.
The back face starts with transform: rotateY(180deg) so it faces away from you initially. When the card flips, the front rotates away and the back rotates into view.
Check the preview. You should see 16 dark blue squares in a 4x4 grid, each showing a pink question mark.
Seeing emoji on top of question marks?
This means backface-visibility: hidden is missing or misspelled. Double-check that both .card-front and .card-back have this property in the shared rule.
Also make sure .card-back has transform: rotateY(180deg) so it starts hidden.
Help me fix my card faces overlapping.
Style the win message
The win message is hidden by default and only appears when JavaScript adds a visible class after the player matches all pairs.
- Add the win message styles at the bottom of styles.css:
/* Hidden by default, shown when player wins */
.win-message {
margin-top: 30px;
text-align: center;
display: none;
}
.win-message.visible {
display: block;
}
.win-message p {
font-size: 1.5rem;
color: #4ecca3;
font-weight: bold;
}
What does this code do?
The display: none hides the win message completely until the game is won. When JavaScript adds the .visible class, the message appears in teal (#4ecca3) to celebrate the win.
- Save styles.css (StackBlitz auto-saves, but press Ctrl+S to be sure).
- Confirm the preview shows a 4x4 grid of dark blue cards with pink question marks on a dark background.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete styles.css file. Compare it with yours to find any differences.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background-color: #1a1a2e;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2rem;
margin-bottom: 10px;
color: #e94560;
}
.stats {
font-size: 1.2rem;
color: #a0a0b0;
}
.game-board {
display: grid;
grid-template-columns: repeat(4, 100px);
gap: 12px;
max-width: 460px;
}
.card {
width: 100px;
height: 100px;
perspective: 600px;
cursor: pointer;
}
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s ease;
}
.card.flipped .card-inner {
transform: rotateY(180deg);
}
.card-front,
.card-back {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 2rem;
}
.card-front {
background-color: #16213e;
border: 2px solid #0f3460;
color: #e94560;
font-weight: bold;
}
.card-back {
background-color: #0f3460;
border: 2px solid #e94560;
transform: rotateY(180deg);
font-size: 2.5rem;
}
.card.matched .card-inner {
transform: rotateY(180deg);
}
.card.matched .card-back {
border-color: #4ecca3;
background-color: #1a3a4a;
}
.win-message {
margin-top: 30px;
text-align: center;
display: none;
}
.win-message.visible {
display: block;
}
.win-message p {
font-size: 1.5rem;
color: #4ecca3;
font-weight: bold;
}
Your game board looks polished and ready for action. Next up, you will write the JavaScript that makes those cards actually flip, detect matches, and track your moves.
Write the JavaScript Game Logic
Your game board looks great, but clicking a card does nothing. That's because HTML and CSS only handle structure and appearance.
JavaScript is what makes a web page interactive. In this step, you'll write the logic that shuffles cards, detects clicks, compares pairs, and tracks your score.
In this step, get ready to:
- Set up game state variables and a card shuffling function.
- Handle card flipping and match detection logic.
- Wire up event listeners and test the win condition.
Set up game state and card shuffling
Before writing any game logic, your JavaScript needs to know which HTML elements to control. It also needs variables to track the current state of the game as the player clicks cards.
- In StackBlitz, click on script.js in the file list on the left.
- Add the following code to set up DOM references, state variables, and a shuffle function:
const cards = document.querySelectorAll('.card');
const moveCounter = document.getElementById('move-counter');
const winMessage = document.getElementById('win-message');
const winText = document.getElementById('win-text');
let flippedCards = [];
let matchedPairs = 0;
let moves = 0;
let boardLocked = false;
// Randomize card positions on the board
function shuffleCards() {
cards.forEach(function(card) {
var randomOrder = Math.floor(Math.random() * 16);
card.style.order = randomOrder;
});
}
What does this code do?
The first four lines grab references to HTML elements so JavaScript can read and change them later.
- flippedCards holds the one or two cards currently face-up.
- matchedPairs counts how many pairs the player has found so far.
- boardLocked prevents extra clicks while two non-matching cards are flipping back over.
- shuffleCards() assigns each card a random CSS order value, which rearranges their position in the grid without moving them in the HTML.
You won't see any change in the preview yet. These variables and the shuffle function activate once we wire up event listeners in the final substep.
Handle card flipping and match detection
Now you need two functions that work together. The first function handles what happens when a player clicks a card. The second function compares two flipped cards to see if they match.
- In script.js, add the flipCard function below your shuffleCards function:
// Handle a card click
function flipCard() {
// Ignore clicks when board is locked or card is already revealed
if (boardLocked) return;
if (this.classList.contains('flipped')) return;
if (this.classList.contains('matched')) return;
// Flip this card face-up
this.classList.add('flipped');
flippedCards.push(this);
// When two cards are face-up, check if they match
if (flippedCards.length === 2) {
moves++;
moveCounter.textContent = moves;
checkForMatch();
}
}
What does this code do?
This function runs every time a card is clicked. The three if ... return lines are guards that exit early if the click should be ignored.
If the click is valid, it adds the flipped class (triggering the CSS animation) and pushes the card into the flippedCards array. Once two cards are face-up, it increments the move counter and calls checkForMatch().
- Now add the checkForMatch function directly below flipCard:
// Compare two flipped cards
function checkForMatch() {
var cardOne = flippedCards[0];
var cardTwo = flippedCards[1];
// If both cards have the same symbol, it's a match
if (cardOne.dataset.symbol === cardTwo.dataset.symbol) {
cardOne.classList.add('matched');
cardTwo.classList.add('matched');
flippedCards = [];
matchedPairs++;
// Check if all 8 pairs have been found
if (matchedPairs === 8) {
winText.textContent = 'Congratulations! You found all pairs in ' + moves + ' moves!';
winMessage.classList.add('visible');
}
} else {
// Not a match - flip both cards back after 1 second
boardLocked = true;
setTimeout(function() {
cardOne.classList.remove('flipped');
cardTwo.classList.remove('flipped');
flippedCards = [];
boardLocked = false;
}, 1000);
}
}
What does this code do?
This function reads the data-symbol attribute from both flipped cards and compares them.
- If the symbols match, both cards get the matched class and stay face-up permanently. The matchedPairs counter increments.
- If all 8 pairs are found, the win message appears with the player's total moves.
- If the symbols don't match, boardLocked prevents extra clicks while setTimeout waits 1 second before flipping both cards back over.
The game logic is written but not connected yet. The final substep activates everything.
Implement the win condition
Your functions are defined, but nothing actually calls them. You need to attach click event listeners to every card and run the shuffle when the page loads.
- At the bottom of script.js, add the event listeners and shuffle call:
// Attach click handler to every card
cards.forEach(function(card) {
card.addEventListener('click', flipCard);
});
// Shuffle cards when the page first loads
shuffleCards();
What does this code do?
The forEach loop attaches a click event listener to each card element. Whenever a card is clicked, the browser runs flipCard.
The final shuffleCards() call runs immediately on page load, so the cards start in a random order every time.
- Save script.js by pressing Ctrl+S.
- Look at the preview panel on the right. The cards should now appear shuffled in a random order.
- Click any card. It should flip over with a smooth animation, revealing the emoji on its back face.
- Click a second card. If the symbols match, both cards stay face-up and turn green. If they don't match, both cards flip back after one second.
- Check that the Moves counter at the top increases each time you flip a second card.
- Play until you find all 8 pairs. A green congratulations message should appear at the bottom showing your total moves.
Cards not flipping when you click?
- Make sure the cards.forEach and shuffleCards() lines are at the very bottom of script.js, outside of any function.
- Check that your function names match exactly: flipCard (not flipcard or FlipCard). JavaScript is case-sensitive.
- Look for red underlines or error messages in the StackBlitz console panel at the bottom. A missing bracket or semicolon can silently break the entire file.
Help me debug why my cards won't flip when I click them.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and that your game is fully playable in the preview.
ⓧ I'd like to double check the full code
Here's the complete script.js file. Compare yours against it line by line.
const cards = document.querySelectorAll('.card');
const moveCounter = document.getElementById('move-counter');
const winMessage = document.getElementById('win-message');
const winText = document.getElementById('win-text');
let flippedCards = [];
let matchedPairs = 0;
let moves = 0;
let boardLocked = false;
function shuffleCards() {
cards.forEach(function(card) {
var randomOrder = Math.floor(Math.random() * 16);
card.style.order = randomOrder;
});
}
function flipCard() {
if (boardLocked) return;
if (this.classList.contains('flipped')) return;
if (this.classList.contains('matched')) return;
this.classList.add('flipped');
flippedCards.push(this);
if (flippedCards.length === 2) {
moves++;
moveCounter.textContent = moves;
checkForMatch();
}
}
function checkForMatch() {
var cardOne = flippedCards[0];
var cardTwo = flippedCards[1];
if (cardOne.dataset.symbol === cardTwo.dataset.symbol) {
cardOne.classList.add('matched');
cardTwo.classList.add('matched');
flippedCards = [];
matchedPairs++;
if (matchedPairs === 8) {
winText.textContent = 'Congratulations! You found all pairs in ' + moves + ' moves!';
winMessage.classList.add('visible');
}
} else {
boardLocked = true;
setTimeout(function() {
cardOne.classList.remove('flipped');
cardTwo.classList.remove('flipped');
flippedCards = [];
boardLocked = false;
}, 1000);
}
}
cards.forEach(function(card) {
card.addEventListener('click', flipCard);
});
shuffleCards();
Your memory card game is fully playable. Next up, you'll deploy it to the internet so anyone can play it from a shareable URL.
Deploy Your Game to the Internet
Your memory card game is fully built and running in the StackBlitz preview. The next step is putting it on the real internet where anyone with the link can play it.
You'll download your project files from StackBlitz, then upload them to Netlify, a free hosting service. Netlify gives you a live URL in seconds with zero configuration.
In this step, get ready to:
- Download your project from StackBlitz as a zip file.
- Deploy to Netlify Drop.
- Verify your live game works at its public URL.
Download your project from StackBlitz
StackBlitz runs entirely in the browser, so to deploy your files elsewhere you first need to download them to your computer.
- In your StackBlitz editor, click the Share button in the top-left navigation bar.
- In the dialog that appears, look for the download button right underneath the Share dialogue.
- Click it to download the project to your computer.
- The project downloads as a .zip file to your Downloads folder.
What is a zip file?
A zip file is a compressed archive that bundles multiple files into one. Your downloaded zip contains index.html, styles.css, script.js, and other StackBlitz files. You need to extract (unzip) it before uploading to Netlify.
- Open your Downloads folder in File Explorer.
- Right-click the downloaded zip file.
- Select Extract All.
- Click Extract in the dialog that appears.
- A new folder opens with your extracted project files.
- Confirm that you can see index.html, styles.css, and script.js inside the extracted folder.
Don't see your files after extracting?
Some zip files extract into a nested folder. If you open the extracted folder and only see another folder inside, open that inner folder. Your index.html file should be at the top level of whatever folder you deploy.
Help me find my extracted project files.
Deploy to Netlify Drop
Netlify Drop lets you deploy a website by simply dragging a folder into your browser. No command line tools or Git knowledge required.
- Open a new browser tab and go to https://app.netlify.com/drop.
- You should see a drop zone area where you can upload your project.
- Go back to File Explorer and open the extracted project folder so you can see your files.
- Drag the entire project folder onto the Netlify Drop zone in your browser.
- Wait for the upload to complete.
Why drag the folder and not individual files?
Netlify needs the folder containing an index.html file at its root level. By dragging the whole folder, Netlify can find your HTML file and link it to your CSS and JavaScript automatically.
Upload not working?
Make sure you are dragging the folder that directly contains index.html at its top level. If your index.html is nested inside a subfolder, drag that inner folder instead.
Also confirm you are signed in to Netlify. If you see a sign-in prompt, sign in with GitHub first, then return to the Drop page.
Help me deploy my folder to Netlify Drop.
Verify your live game
Once the upload finishes, Netlify creates a live website for you instantly.
- Netlify provides a URL ending in .netlify.app. Find it on the page after your upload completes.
- Click the URL to open your live game in a new tab.
- Test that your cards flip when you click them.
- Test that matching pairs stay face-up with a green border.
- Test that the move counter increments each time you flip two cards.
- Match all 8 pairs to confirm the win message appears.
You should see a green congratulations message at the bottom when you match all pairs.
Your game is on the internet
That URL is a real website anyone can visit. Copy it and send it to a friend to let them play your memory card game. The site stays live on Netlify's free tier indefinitely.
Cards not flipping on the live site?
Make sure your script.js file was included in the folder you uploaded. Open your browser's developer tools by pressing F12 and check the Console tab for any errors shown in red.
If you see a file-not-found error, your folder structure may have been nested. Re-drag the correct inner folder that contains index.html directly.
Help me fix my deployed game that isn't working.
Secret mission
Add a Timer and Restart Button
Your memory game works, but there is no pressure. What if the player had only 60 seconds to find all the pairs? Add a countdown timer that creates urgency and a restart button that reshuffles the board for a fresh round.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on free services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- StackBlitz project (free, persists in your account).
- Netlify site (free tier, stays online indefinitely unless deleted).
- Downloaded project files in your Downloads folder.
Keep everything running
No action needed. Choose this if you're still actively building or want your game to stay live.
- Your game stays live at its .netlify.app URL at no cost.
- Your StackBlitz project remains available in your dashboard for future editing.
- The downloaded zip and extracted folder remain in your Downloads folder.
Pause - I'll come back to this later
Take your site offline while keeping the code and configuration intact.
- In Netlify, click on your site from the dashboard.
- Go to Project configuration > General > Danger zone.
- Select Disable project. This takes your site offline but keeps all configuration saved.
- Your StackBlitz project stays in your account for future editing.
- When you're ready to come back, return to the same Danger zone section and select Enable project to bring your site back online.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- In Netlify, click on your site from the dashboard.
- Go to Project configuration > General > Danger zone.
- Select Delete this site and confirm when prompted.
- In StackBlitz, open your project and click the Settings sidebar. Select the option to delete the project.
- On your computer, open File Explorer and navigate to your Downloads folder. Delete the downloaded zip file and the extracted project folder.
Nice Work!
Nice Work!
You just built a fully interactive memory card game from scratch and deployed it live to the internet. Anyone with the link can play your game right now.
You've learned how to:
- Build a complete memory card game from scratch using HTML, CSS, and JavaScript.
- Use CSS Grid for responsive layout and CSS 3D transforms for card flip animations.
- Deploy a live website to the internet using Netlify Drop with a shareable .netlify.app URL.
- Secret Mission: Added a countdown timer with setInterval and a restart button that resets game state.
Ready to quiz yourself?