Create an Engaging Memory Card Game with Vanilla JavaScript
Learn Computer Academy

Learn Computer Academy @learncomputer

About: Website Design and Development Training Institute in Habra We provide the best hands-on training in Graphics Design, Website Design and Development.

Location:
Habra, India
Joined:
Mar 7, 2025

Create an Engaging Memory Card Game with Vanilla JavaScript

Publish Date: Mar 29
0 0

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.

Screenshot of Memory Match Master Game

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>
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 = `
            <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();
    }
});
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:

  1. Determine grid size from difficulty selection
  2. Create an array of emoji pairs
  3. Shuffle the array to randomize card positions
  4. 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:

  1. flipCard() - Handles the card flipping interaction and tracks which cards are flipped
  2. 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:

  1. Progress Bar - Visually shows completion percentage
  2. Hint System - Reveals a matching pair briefly (limited to 3 uses)
  3. 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!

Comments 0 total

    Add comment