Hey devs! Today I want to share a fun project I recently built - a memory card matching game using vanilla JavaScript, CSS, and HTML. No frameworks, no libraries, just pure web fundamentals with some modern techniques thrown in!
You can check out the live demo here: Memory Match Master
The Project Overview
"Memory Match Master" is a classic card-matching game that challenges players to find matching pairs of cards while tracking their performance. It's the perfect project to practice core web development concepts while creating something entertaining.
Key Features
This game includes several features that make it both fun to play and educational to build:
- 🎯 Multiple difficulty levels (4x4, 6x4, and 6x6 grids)
- ⏱️ Timer to track gameplay duration
- 🔢 Move counter with scoring system
- 📊 Visual progress bar
- 💡 Hint system with limited uses
- 🌓 Dark/light theme toggle
- 📱 Responsive design for all devices
The HTML Structure
The HTML foundation is straightforward but comprehensive. It includes containers for the game board, controls, and a modal for the game-over state.
</span>
lang="en">
charset="UTF-8">
name="viewport" content="width=device-width, initial-scale=1.0">
Memory Match Master
rel="stylesheet" href="styles.css">
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
class="game-container">
Memory Match Master
class="stats">
Time: id="timer">00:00
Moves: id="moves">0
Score: id="score">0
class="progress-container">
id="progress-bar" class="progress-bar">
class="how-to-play">
How to Play
Flip two cards at a time to find matching pairs. Match all pairs to win!
Difficulty: Choose Easy (4x4), Medium (6x4), or Hard (6x6).
Moves: Each pair flip counts as one move. Fewer moves = higher score.
Score: Earn 100 points per match, minus moves taken.
Hints: Use up to 3 hints to reveal a pair briefly.
Timer: Track how long it takes to complete the game.
class="controls">
id="difficulty">
value="easy">Easy (4x4)
value="medium">Medium (6x4)
value="hard">Hard (6x6)
id="start-btn">Start Game
id="hint-btn">Hint (3)
id="theme-toggle">Dark Mode
id="game-board" class="game-board">
id="modal" class="modal">
class="modal-content">
Game Over!
Your Score: id="final-score">
id="restart-btn">Play Again
<span class="na">src="script.js">
Enter fullscreen mode
Exit fullscreen mode
The structure prioritizes semantic elements and clear organization. I've included a "How to Play" section directly in the interface to make the game immediately accessible to new players.
CSS Magic
The styling is where things get interesting. I used modern CSS techniques to create smooth animations and responsive layouts:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background: linear-gradient(135deg, #74ebd5, #acb6e5);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
transition: background 0.5s;
}
body.dark {
background: linear-gradient(135deg, #1f1c2c, #928dab);
}
.game-container {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 800px;
text-align: center;
}
body.dark .game-container {
background: rgba(40, 40, 40, 0.95);
color: #fff;
}
header h1 {
font-size: 2.5em;
color: #333;
margin-bottom: 10px;
}
body.dark header h1 {
color: #fff;
}
.stats {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
font-size: 1.2em;
color: #555;
}
body.dark .stats {
color: #ddd;
}
.progress-container {
width: 80%;
height: 10px;
background: #ddd;
border-radius: 5px;
margin: 10px auto;
overflow: hidden;
}
body.dark .progress-container {
background: #555;
}
.progress-bar {
height: 100%;
width: 0;
background: #6a82fb;
border-radius: 5px;
transition: width 0.3s ease-in-out;
}
body.dark .progress-bar {
background: #fc5c7d;
}
.how-to-play {
margin-bottom: 20px;
text-align: left;
padding: 15px;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
body.dark .how-to-play {
background: rgba(60, 60, 60, 0.8);
}
.how-to-play h2 {
font-size: 1.5em;
color: #333;
margin-bottom: 10px;
}
body.dark .how-to-play h2 {
color: #fff;
}
.how-to-play p {
font-size: 1em;
color: #555;
margin-bottom: 10px;
}
body.dark .how-to-play p {
color: #ddd;
}
.how-to-play ul {
list-style: none;
color: #555;
}
body.dark .how-to-play ul {
color: #ddd;
}
.how-to-play li {
margin: 5px 0;
}
.how-to-play strong {
color: #6a82fb;
}
body.dark .how-to-play strong {
color: #fc5c7d;
}
.controls {
margin-bottom: 20px;
}
select, button {
padding: 10px 20px;
margin: 0 10px;
border: none;
border-radius: 25px;
background: #6a82fb;
color: white;
font-size: 1em;
cursor: pointer;
transition: transform 0.2s, background 0.3s;
}
select:hover, button:hover {
transform: scale(1.05);
background: #fc5c7d;
}
.game-board {
display: grid;
gap: 10px;
justify-content: center;
}
.card {
width: 80px;
height: 80px;
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
position: relative;
transform-style: preserve-3d;
transition: transform 0.5s;
cursor: pointer;
}
body.dark .card {
background: #444;
}
.card.flipped {
transform: rotateY(180deg);
}
.card.matched {
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0% { transform: rotateY(180deg) scale(1); }
50% { transform: rotateY(180deg) scale(1.1); }
100% { transform: rotateY(180deg) scale(1); }
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
border-radius: 10px;
}
.card-front {
background: #fc5c7d;
color: white;
transform: rotateY(180deg);
}
.card-back {
background: #6a82fb;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
body.dark .modal-content {
background: #333;
color: #fff;
}
Enter fullscreen mode
Exit fullscreen mode
Some of the CSS highlights include:
CSS Grid for the game board layout
3D transforms for card flipping animations
CSS variables for theme switching
Flexbox for responsive controls
Gradient backgrounds that smoothly transition between themes
Keyframe animations for matched cards
The card flip effect deserves special attention. By combining transform-style: preserve-3d with proper backface visibility management, we create a realistic card-flipping experience that feels tactile despite being entirely in CSS.
JavaScript Game Logic
Now for the fun part - bringing the game to life with JavaScript:
const gameBoard = document.getElementById('game-board');
const timerDisplay = document.getElementById('timer');
const movesDisplay = document.getElementById('moves');
const scoreDisplay = document.getElementById('score');
const startBtn = document.getElementById('start-btn');
const hintBtn = document.getElementById('hint-btn');
const themeToggle = document.getElementById('theme-toggle');
const difficultySelect = document.getElementById('difficulty');
const modal = document.getElementById('modal');
const finalScore = document.getElementById('final-score');
const restartBtn = document.getElementById('restart-btn');
const progressBar = document.getElementById('progress-bar');
let cards = [];
let flippedCards = [];
let matchedPairs = 0;
let moves = 0;
let score = 0;
let time = 0;
let timer;
let hintsLeft = 3;
let gridSize;
const emojis = ['🐱', '🐶', '🐻', '🦁', '🐼', '🦊', '🐰', '🐸', '🐷', '🐵', '🦄', '🐙'];
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function createBoard() {
gameBoard.innerHTML = '';
const difficulty = difficultySelect.value;
gridSize = difficulty === 'easy' ? [4, 4] : difficulty === 'medium' ? [6, 4] : [6, 6];
const totalCards = gridSize[0] * gridSize[1];
const pairCount = totalCards / 2;
const cardValues = shuffle([...emojis.slice(0, pairCount), ...emojis.slice(0, pairCount)]);
gameBoard.style.gridTemplateColumns = `repeat(${gridSize[1]}, 80px)`;
cards = cardValues.map((value, index) => {
const card = document.createElement('div');
card.classList.add('card');
card.innerHTML = `
${value}
`;
card.addEventListener('click', () => flipCard(card, value));
gameBoard.appendChild(card);
return card;
});
updateProgress();
}
function flipCard(card, value) {
if (flippedCards.length < 2 && !card.classList.contains('flipped') && !card.classList.contains('matched')) {
card.classList.add('flipped');
flippedCards.push({ card, value });
moves++;
movesDisplay.textContent = moves;
if (flippedCards.length === 2) {
checkMatch();
}
}
}
function checkMatch() {
const [card1, card2] = flippedCards;
if (card1.value === card2.value) {
card1.card.classList.add('matched');
card2.card.classList.add('matched');
matchedPairs++;
score += 100 - moves;
scoreDisplay.textContent = score;
updateProgress();
if (matchedPairs === (gridSize[0] * gridSize[1]) / 2) {
endGame();
}
} else {
setTimeout(() => {
card1.card.classList.remove('flipped');
card2.card.classList.remove('flipped');
}, 1000);
}
flippedCards = [];
}
function updateProgress() {
const totalPairs = (gridSize[0] * gridSize[1]) / 2;
const progress = (matchedPairs / totalPairs) * 100;
progressBar.style.width = `${progress}%`;
}
function startTimer() {
clearInterval(timer);
time = 0;
timer = setInterval(() => {
time++;
const minutes = Math.floor(time / 60).toString().padStart(2, '0');
const seconds = (time % 60).toString().padStart(2, '0');
timerDisplay.textContent = `${minutes}:${seconds}`;
}, 1000);
}
function endGame() {
clearInterval(timer);
finalScore.textContent = score;
modal.style.display = 'flex';
}
function useHint() {
if (hintsLeft > 0 && flippedCards.length === 0) {
hintsLeft--;
hintBtn.textContent = `Hint (${hintsLeft})`;
const unmatched = cards.filter(card => !card.classList.contains('matched'));
const valueToMatch = unmatched[0].querySelector('.card-front').textContent;
const matches = unmatched.filter(card => card.querySelector('.card-front').textContent === valueToMatch);
matches.forEach(card => {
card.classList.add('flipped');
setTimeout(() => card.classList.remove('flipped'), 1000);
});
}
}
startBtn.addEventListener('click', () => {
moves = 0;
score = 0;
matchedPairs = 0;
hintsLeft = 3;
movesDisplay.textContent = moves;
scoreDisplay.textContent = score;
hintBtn.textContent = `Hint (${hintsLeft})`;
progressBar.style.width = '0%';
createBoard();
startTimer();
});
hintBtn.addEventListener('click', useHint);
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark');
themeToggle.textContent = document.body.classList.contains('dark') ? 'Light Mode' : 'Dark Mode';
});
restartBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to restart the game?')) {
modal.style.display = 'none';
startBtn.click();
}
});
Enter fullscreen mode
Exit fullscreen mode
Let's break down how the game works:
Dynamic Board Generation
Instead of hardcoding cards, we dynamically generate the game board based on the selected difficulty. This makes the code more maintainable and flexible:
Determine grid size from difficulty selection
Create an array of emoji pairs
Shuffle the array to randomize card positions
Generate and append card elements to the DOM
Game State Management
The game tracks several state variables:
Cards that are currently flipped
Pairs that have been matched
Number of moves made
Score calculation
Remaining hints
Elapsed time
The Card Matching Logic
The core game logic happens in two main functions:
flipCard() - Handles the card flipping interaction and tracks which cards are flipped
checkMatch() - Determines if two flipped cards match and updates the game state accordingly
When a player flips two cards that match, we:
Mark them as matched
Update the score (100 points per match, minus the number of moves)
Update the progress bar
Check if all pairs are found
When cards don't match, we flip them back after a brief delay to give the player time to memorize their positions.
Player Assistance Features
I added some quality-of-life features to enhance the gameplay:
Progress Bar - Visually shows completion percentage
Hint System - Reveals a matching pair briefly (limited to 3 uses)
Theme Toggle - Switches between light and dark modes for comfortable play in any environment
Technical Challenges & Solutions
Building this game presented some interesting challenges:
Challenge: Card Flipping Animation
Creating a smooth, realistic card flip that works across browsers took some experimentation. The solution combines 3D transforms with proper timing and event handling.
Challenge: Score Calculation
I wanted a scoring system that rewards efficiency. The final formula (100 points per match minus total moves) encourages strategic play rather than random clicking.
Challenge: Responsive Design
Making the game work well on both small mobile screens and large desktops required careful planning of the layout and grid sizing.
What I Learned
This project reinforced my understanding of:
DOM manipulation without relying on libraries
Event handling and timing functions
CSS animations and transitions
Game state management
User experience considerations
Possible Future Enhancements
I'm considering several upgrades for future versions:
Persistent high scores using localStorage
Custom card themes beyond emojis
Sound effects for interactions
Keyboard controls for accessibility
Multiplayer capabilities
Conclusion
Building a memory game from scratch is both fun and educational. It combines visual design, animation techniques, and game logic in a project that's approachable for intermediate developers but still offers plenty of learning opportunities.The code is modular enough that you can easily customize it for your own needs or extend it with additional features.What memory-based games have you built? Have you tried creating games with vanilla JS? Let me know in the comments!Check out the live demo at https://playground.learncomputer.in/memory-card-game/ and feel free to fork the project!