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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Match Master</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="game-container">
<header>
<h1>Memory Match Master</h1>
<div class="stats">
<span>Time: <span id="timer">00:00</span></span>
<span>Moves: <span id="moves">0</span></span>
<span>Score: <span id="score">0</span></span>
</div>
<div class="progress-container">
<div id="progress-bar" class="progress-bar"></div>
</div>
</header>
<div class="how-to-play">
<h2>How to Play</h2>
<p>Flip two cards at a time to find matching pairs. Match all pairs to win!</p>
<ul>
<li><strong>Difficulty:</strong> Choose Easy (4x4), Medium (6x4), or Hard (6x6).</li>
<li><strong>Moves:</strong> Each pair flip counts as one move. Fewer moves = higher score.</li>
<li><strong>Score:</strong> Earn 100 points per match, minus moves taken.</li>
<li><strong>Hints:</strong> Use up to 3 hints to reveal a pair briefly.</li>
<li><strong>Timer:</strong> Track how long it takes to complete the game.</li>
</ul>
</div>
<div class="controls">
<select id="difficulty">
<option value="easy">Easy (4x4)</option>
<option value="medium">Medium (6x4)</option>
<option value="hard">Hard (6x6)</option>
</select>
<button id="start-btn">Start Game</button>
<button id="hint-btn">Hint (3)</button>
<button id="theme-toggle">Dark Mode</button>
</div>
<div id="game-board" class="game-board"></div>
<div id="modal" class="modal">
<div class="modal-content">
<h2>Game Over!</h2>
<p>Your Score: <span id="final-score"></span></p>
<button id="restart-btn">Play Again</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
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;
}
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 = `
<div class="card-back"></div>
<div class="card-front">${value}</div>
`;
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();
}
});
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!
Top comments (0)