Building a Memory Card Game with HTML, CSS, and JavaScript

Creating a memory card game is an excellent project for web developers of all skill levels. It combines fundamental web technologies with game logic to create something both fun and educational. In this guide, I'll walk you through building a complete memory card matching game from scratch.
What We're Building
We'll create a classic memory matching game where:
Cards are placed face down on the board
Players flip two cards at a time
If the cards match, they stay flipped
If they don't match, they flip back over
The game ends when all cards are matched
We'll track moves and time to add challenge
Project Setup
First, let's create our project structure:
memory-game/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── script.js
└── images/
├── back.png
└── (card images 1-8)
HTML Structure
Let's start with the HTML structure for our game. Create an index.html 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 rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>Memory Card Game</h1>
<div class="game-info">
<div class="game-stat">
<span class="stat-label">Time:</span>
<span id="time">00:00</span>
</div>
<div class="game-stat">
<span class="stat-label">Moves:</span>
<span id="moves">0</span>
</div>
<button id="restart">Restart Game</button>
</div>
<div id="game-board">
<!-- Cards will be generated by JavaScript -->
</div>
<div id="game-over" class="hidden">
<div class="overlay"></div>
<div class="game-over-content">
<h2>Congratulations!</h2>
<p>You've completed the game in <span id="final-time">00:00</span> with <span id="final-moves">0</span> moves.</p>
<button id="play-again">Play Again</button>
</div>
</div>
</div>
<script src="js/script.js"></script>
</body>
</html>
This creates a simple structure with:
A title
Game statistics (time and moves counter)
A restart button
The game board where cards will appear
A hidden "game over" modal that will be displayed when the player wins
CSS Styling
Now, let's style our game. Create a file named style.css in the css folder:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.game-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 15px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.game-stat {
font-size: 18px;
}
.stat-label {
font-weight: bold;
margin-right: 5px;
color: #3498db;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
#game-board {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 15px;
perspective: 1000px;
}
.card {
height: 150px;
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s;
cursor: pointer;
}
.card.flipped {
transform: rotateY(180deg);
}
.card.matched {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-front {
background-color: #2c3e50;
transform: rotateY(180deg);
}
.card-back {
background-color: #3498db;
background-image: url('../images/back.png');
background-size: cover;
background-position: center;
}
.card-front img {
max-width: 80%;
max-height: 80%;
}
.hidden {
display: none;
}
#game-over .overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.game-over-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 30px;
border-radius: 10px;
text-align: center;
z-index: 2;
max-width: 400px;
width: 90%;
}
.game-over-content h2 {
color: #2c3e50;
margin-bottom: 20px;
}
.game-over-content p {
margin-bottom: 20px;
font-size: 18px;
}
#play-again {
margin-top: 10px;
}
/* Make it responsive */
@media (max-width: 600px) {
#game-board {
grid-template-columns: repeat(3, 1fr);
}
.card {
height: 120px;
}
.game-info {
flex-direction: column;
gap: 10px;
}
}
@media (max-width: 400px) {
#game-board {
grid-template-columns: repeat(2, 1fr);
}
}
This CSS:
Creates a clean, modern layout
Styles the cards with 3D flipping animations using CSS transforms
Makes the game responsive for different screen sizes
Styles the game info panel and game over modal
JavaScript Implementation
Now for the most important part - the game logic. Create a file named script.js in the js folder:
document.addEventListener('DOMContentLoaded', () => {
// Game variables
let hasFlippedCard = false;
let lockBoard = false;
let firstCard, secondCard;
let moves = 0;
let matches = 0;
let timeElapsed = 0;
let timerInterval;
let gameStarted = false;
// DOM elements
const gameBoard = document.getElementById('game-board');
const movesElement = document.getElementById('moves');
const timeElement = document.getElementById('time');
const restartButton = document.getElementById('restart');
const gameOverElement = document.getElementById('game-over');
const finalTimeElement = document.getElementById('final-time');
const finalMovesElement = document.getElementById('final-moves');
const playAgainButton = document.getElementById('play-again');
// Card data - in a real project, you might want to use actual image files
const cardData = [
{ name: 'apple', image: '🍎' },
{ name: 'banana', image: '🍌' },
{ name: 'orange', image: '🍊' },
{ name: 'strawberry', image: '🍓' },
{ name: 'watermelon', image: '🍉' },
{ name: 'grape', image: '🍇' },
{ name: 'pear', image: '🍐' },
{ name: 'pineapple', image: '🍍' }
];
// Initialize game
initGame();
// Event listeners
restartButton.addEventListener('click', restartGame);
playAgainButton.addEventListener('click', restartGame);
// Functions
function initGame() {
// Create the cards array with pairs
const cards = [...cardData, ...cardData];
// Shuffle cards
shuffleCards(cards);
// Generate the card elements
generateCards(cards);
}
function shuffleCards(cards) {
for (let i = cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[cards[i], cards[j]] = [cards[j], cards[i]]; // Swap elements
}
return cards;
}
function generateCards(cards) {
// Clear game board
gameBoard.innerHTML = '';
// Create card elements
cards.forEach((card, index) => {
const cardElement = document.createElement('div');
cardElement.classList.add('card');
cardElement.dataset.name = card.name;
cardElement.dataset.index = index;
const cardFront = document.createElement('div');
cardFront.classList.add('card-face', 'card-front');
// Use text emojis for simplicity, but you could use images instead
cardFront.innerHTML = `<span style="font-size: 50px;">${card.image}</span>`;
const cardBack = document.createElement('div');
cardBack.classList.add('card-face', 'card-back');
cardElement.appendChild(cardFront);
cardElement.appendChild(cardBack);
cardElement.addEventListener('click', flipCard);
gameBoard.appendChild(cardElement);
});
}
function flipCard() {
if (lockBoard) return;
if (this === firstCard) return;
this.classList.add('flipped');
// Start timer on first card flip
if (!gameStarted) {
startTimer();
gameStarted = true;
}
if (!hasFlippedCard) {
// First card flipped
hasFlippedCard = true;
firstCard = this;
return;
}
// Second card flipped
secondCard = this;
moves++;
movesElement.textContent = moves;
checkForMatch();
}
function checkForMatch() {
let isMatch = firstCard.dataset.name === secondCard.dataset.name;
if (isMatch) {
disableCards();
matches++;
// Check if all pairs are matched
if (matches === cardData.length) {
endGame();
}
} else {
unflipCards();
}
}
function disableCards() {
firstCard.removeEventListener('click', flipCard);
secondCard.removeEventListener('click', flipCard);
firstCard.classList.add('matched');
secondCard.classList.add('matched');
resetBoard();
}
function unflipCards() {
lockBoard = true;
setTimeout(() => {
firstCard.classList.remove('flipped');
secondCard.classList.remove('flipped');
resetBoard();
}, 1000);
}
function resetBoard() {
[hasFlippedCard, lockBoard] = [false, false];
[firstCard, secondCard] = [null, null];
}
function startTimer() {
clearInterval(timerInterval);
timeElapsed = 0;
timerInterval = setInterval(() => {
timeElapsed++;
updateTimeDisplay();
}, 1000);
}
function updateTimeDisplay() {
const minutes = Math.floor(timeElapsed / 60).toString().padStart(2, '0');
const seconds = (timeElapsed % 60).toString().padStart(2, '0');
timeElement.textContent = `${minutes}:${seconds}`;
}
function endGame() {
clearInterval(timerInterval);
// Update final stats in the game over modal
const minutes = Math.floor(timeElapsed / 60).toString().padStart(2, '0');
const seconds = (timeElapsed % 60).toString().padStart(2, '0');
finalTimeElement.textContent = `${minutes}:${seconds}`;
finalMovesElement.textContent = moves;
// Show game over modal with a slight delay
setTimeout(() => {
gameOverElement.classList.remove('hidden');
}, 500);
}
function restartGame() {
// Reset game variables
hasFlippedCard = false;
lockBoard = false;
firstCard = null;
secondCard = null;
moves = 0;
matches = 0;
timeElapsed = 0;
gameStarted = false;
// Reset DOM elements
movesElement.textContent = moves;
updateTimeDisplay();
gameOverElement.classList.add('hidden');
// Clear timer
clearInterval(timerInterval);
// Initialize game again
initGame();
}
});
Let's break down what this JavaScript code does:
Game Variables: Tracks the game state, including flipped cards, moves, and time.
Card Generation and Shuffling:
Creates card pairs
Shuffles them randomly
Generates HTML elements for each card
Game Mechanics:
Card flipping logic
Matching logic
Timer and score tracking
Win Condition:
Detects when all pairs are matched
Displays the game over modal with final stats
Restart Function:
Resets all game variables
Re-initializes the board with shuffled cards
Final Touches and Enhancements
Adding Card Images
In our example, we used emojis for simplicity, but you can replace them with actual images:
// Example with image paths instead of emojis
const cardData = [
{ name: 'apple', image: 'images/apple.png' },
{ name: 'banana', image: 'images/banana.png' },
// ...other images
];
// Then in the generateCards function:
const imgElement = document.createElement('img');
imgElement.src = card.image;
imgElement.alt = card.name;
cardFront.appendChild(imgElement);
Adding Sound Effects
Sound effects can enhance the gaming experience:
// Create audio elements
const flipSound = new Audio('sounds/flip.mp3');
const matchSound = new Audio('sounds/match.mp3');
const victorySound = new Audio('sounds/victory.mp3');
// Play flip sound in flipCard function
flipSound.play();
// Play match sound when cards match
matchSound.play();
// Play victory sound when game ends
victorySound.play();
Adding Difficulty Levels
You can introduce different difficulty levels by changing the number of cards:
function initGame(difficulty = 'medium') {
let cardSubset;
switch(difficulty) {
case 'easy':
cardSubset = cardData.slice(0, 4); // 8 cards (4 pairs)
break;
case 'medium':
cardSubset = cardData; // 16 cards (8 pairs)
break;
case 'hard':
cardSubset = [...cardData,
{ name: 'cherry', image: '🍒' },
{ name: 'peach', image: '🍑' },
{ name: 'lemon', image: '🍋' },
{ name: 'mango', image: '🥭' }
]; // 24 cards (12 pairs)
break;
}
// Create pairs and initialize as before
const cards = [...cardSubset, ...cardSubset];
shuffleCards(cards);
generateCards(cards);
// Adjust grid layout based on difficulty
if (difficulty === 'easy') {
gameBoard.style.gridTemplateColumns = 'repeat(3, 1fr)';
} else if (difficulty === 'medium') {
gameBoard.style.gridTemplateColumns = 'repeat(4, 1fr)';
} else {
gameBoard.style.gridTemplateColumns = 'repeat(6, 1fr)';
}
}
Adding Animations
You can enhance the matching animation:
@keyframes pulse {
0% { transform: scale(1) rotateY(180deg); }
50% { transform: scale(1.1) rotateY(180deg); }
100% { transform: scale(1) rotateY(180deg); }
}
.card.matched {
transform: rotateY(180deg);
animation: pulse 0.5s;
}
Conclusion
Congratulations! You've built a fully functional memory card game with HTML, CSS, and JavaScript. This project demonstrates several important web development concepts:
DOM Manipulation: Creating, modifying, and removing elements
Event Handling: Responding to user interactions
CSS Animations: Creating smooth card flipping effects
Game Logic: Implementing rules and win conditions
Responsive Design: Making the game work on different screen sizes
Further Challenges
If you want to take this project further, consider these enhancements:
Leaderboard: Save and display high scores using localStorage
Themes: Allow players to choose different card themes
Multiplayer: Add a two-player mode where players take turns
Progressive Difficulty: Increase the number of cards as players advance
Happy coding, and enjoy your memory game!






