Skip to main content

Command Palette

Search for a command to run...

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

Published
8 min readView as Markdown
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:

  1. Game Variables: Tracks the game state, including flipped cards, moves, and time.

  2. Card Generation and Shuffling:

    • Creates card pairs

    • Shuffles them randomly

    • Generates HTML elements for each card

  3. Game Mechanics:

    • Card flipping logic

    • Matching logic

    • Timer and score tracking

  4. Win Condition:

    • Detects when all pairs are matched

    • Displays the game over modal with final stats

  5. 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:

  1. DOM Manipulation: Creating, modifying, and removing elements

  2. Event Handling: Responding to user interactions

  3. CSS Animations: Creating smooth card flipping effects

  4. Game Logic: Implementing rules and win conditions

  5. 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!

More from this blog

L

Learn Computer Academy

50 posts

Website Design and Development Training Center in Habra. We offer top-notch practical training in Graphics Design, Website Design, and Development.