DEV Community

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

Posted on

Create an Engaging Memory Card Game with Vanilla JavaScript

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!

Heroku

Deliver your unique apps, your own way.

Heroku tackles the toil โ€” patching and upgrading, 24/7 ops and security, build systems, failovers, and more. Stay focused on building great data-driven applications.

Learn More

Top comments (0)

Image of Stellar post

Check out Episode 1: How a Hackathon Project Became a Web3 Startup ๐Ÿš€

Ever wondered what it takes to build a web3 startup from scratch? In the Stellar Dev Diaries series, we follow the journey of a team of developers building on the Stellar Network as they go from hackathon win to getting funded and launching on mainnet.

Read more

AWS GenAI LIVE!

GenAI LIVE! is a dynamic live-streamed show exploring how AWS and our partners are helping organizations unlock real value with generative AI.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. โค๏ธ