Memory Match Game — Free HTML CSS JS Snippet

Memory Match Game · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real 3D CSS flip: perspective + preserve-3d + backface-visibility, matching the site's other flip-card snippets
Lock flag prevents a fast third click from corrupting an in-progress match comparison
Different pause durations for match (320ms) vs mismatch (850ms) resolution
Fisher-Yates shuffle reruns on every restart for a genuinely random layout
Live move counter incrementing once per pair attempt, not per single card click
Lazily-started elapsed timer that only begins on the first card flip, not on page load
Lightweight confetti burst built on the native Element.animate() Web Animations API, no canvas or library
Match pulse-glow keyframe animation plus a persistent green accent glow on solved pairs

About this UI Snippet

Memory Match Game — 3D Card Flip, Match-Lock State Machine & Lightweight Confetti Burst in Vanilla JS

Screenshot of the Memory Match Game snippet rendered live

A memory-matching game looks simple — flip two cards, check if they match, repeat — but a naive click handler breaks almost immediately under fast, real-world clicking. This snippet implements the classic game using the same 3D CSS flip mechanics as the site's 3D flip card and flip card modal snippets, paired with a small but essential game-state guard, a move counter, a live elapsed timer, and a hand-rolled particle-burst celebration effect using the Web Animations API rather than a canvas confetti library.

The 3D flip: perspective, preserve-3d, backface-visibility

Each card lives inside a .card-scene wrapper; the grid container itself carries perspective: 900px, establishing the 3D viewing context all cards share. Each .card has transform-style: preserve-3d so its two face children are rendered in that same 3D space rather than flattened. The .face-front is pre-rotated to rotateY(180deg) at rest and both faces use backface-visibility: hidden, so only one face is ever visible at a given rotation — exactly the same four-property combination documented in this library's other 3D flip snippets. Toggling .flipped on the card rotates it 180 degrees on the Y axis, simultaneously turning the back (question-mark) face away from the viewer and bringing the pre-rotated front face around to face them.

Why a lock flag is required, not optional

Without a guard, the click handler has an obvious race condition: a player flips two mismatched cards, sees they do not match, and — before the short "let me memorize this" pause finishes and the mismatched pair auto-flips back — clicks a third card. At that moment flippedCards already holds two entries; a naive implementation would push a third, and either the match-comparison logic silently breaks (comparing the wrong pair, or leaving a card permanently flipped) or the player briefly sees a card face they should not have access to yet, undermining the entire point of the memory challenge. This snippet's onCardClick() defends against exactly that: the moment a second card is flipped, locked is set to true and moves is incremented immediately, before any setTimeout fires. Every subsequent click, no matter how fast, hits the if (locked) return; guard at the very top of onCardClick() and is silently ignored until resolveMatch() or resolveMismatch() finishes and explicitly sets locked = false again. This makes "exactly zero or two cards are ever flipped-and-unresolved at once" a true invariant instead of a hopeful assumption.

Two different pauses for two different outcomes

A match is confirmed after a short 320ms pause (long enough to register visually, short enough not to feel like a stall) before resolveMatch() runs. A mismatch uses a longer 850ms pause before resolveMismatch() flips both cards back — deliberately longer, because the entire point of that pause is giving the player time to actually memorize which icon was where before the cards hide it again. Both pauses funnel through the same locked flag, so the length of the delay never affects correctness, only pacing.

The match pulse-glow and lightweight confetti

A successful match adds .matched, which triggers a pulse-glow keyframe animation (a brief scale-up-and-back on the already-flipped card) and a persistent green glow via box-shadow on the front face. Independently, burstConfetti() reads the card's live getBoundingClientRect() to find its screen-space center, then spawns ten small colored divs positioned at that point and animates each one with the native Element.animate() Web Animations API — no requestAnimationFrame loop, no external confetti library, no canvas. Each bit gets a random angle and distance and animates from its spawn point to that offset with fading opacity and a random rotation, and removes itself from the DOM in the animation's onfinish callback so nothing lingers. Finishing the last pair triggers a larger 40-piece burst from the grid's center as a bigger celebration moment, layered under the win banner that fades in with the final move count and elapsed time.

Move counter and live timer

moves increments once per pair-attempt (not per single card click), matching how memory games conventionally score. The timer starts lazily — startTimerIfNeeded() only fires Date.now() and starts a setInterval on the very first card flip of a game, not on page load — so idle time before a player's first move is never counted, and it is stopped precisely once the final pair matches by reading the same startTime reference used to display the running clock, guaranteeing the final recorded time matches what was on-screen the moment the game ended.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's JS into an AI assistant like Claude and ask it to walk through the exact sequence of events that would go wrong if the locked flag were removed — specifically what a fast third click does to the flippedCards array and why that breaks match detection. It is also worth asking whether resolveMismatch and resolveMatch could be merged into one function with a single "isMatch" branch to reduce duplication, or whether keeping them separate is clearer given their different pause durations. For extending the game, ask for a difficulty selector that changes the grid size and icon count, a best-time leaderboard using localStorage, or a two-player alternating-turns mode where a mismatch passes the turn to the other player.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a memory-matching card game in plain HTML, CSS, and JavaScript using a real 3D CSS card flip — no canvas, no animation library.

Requirements:
- A grid of face-down cards, each built from a perspective-contained wrapper and a card element using transform-style: preserve-3d, with a back face (question mark) and a front face (icon) both using backface-visibility: hidden, the front face pre-rotated to rotateY(180deg) so toggling a single "flipped" class on the card performs the actual flip.
- Clicking a face-down, non-matched card flips it and adds it to a small "currently flipped" list; clicking a second card triggers a comparison after a short delay.
- A "lock" boolean flag that gets set to true the instant a second card is flipped (before any comparison delay runs) and is only cleared after the match or mismatch has fully resolved, with the click handler ignoring all clicks while locked is true. Explain in a comment the exact race condition this prevents: a fast third click landing while two mismatched cards are still shown, before their automatic flip-back has happened.
- Use a noticeably longer pause before flipping mismatched cards back down than before confirming a match, since the mismatch pause is what gives the player time to memorize the two revealed cards.
- On a successful match, play a pulse/glow animation on both matched cards and spawn a small lightweight confetti-style particle burst (a handful of small colored elements animating outward and fading, built with the native Element.animate() Web Animations API rather than a canvas or external library) from the matched cards' position.
- Track and display a move counter (incremented once per pair attempt) and an elapsed timer that starts on the first card flip (not on page load) and stops the moment all pairs are matched.
- When every pair is matched, show a win banner with the final move count and time, plus a larger confetti burst from the center of the board.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click any face-down card to flip itThe card rotates 180 degrees around the Y axis using a real 3D transform, revealing its icon. The move timer starts counting from this first click.
  2. 2
    Click a second card to attempt a matchOnce two cards are flipped, further clicks are briefly ignored while the game checks the pair — this prevents a fast third click from interrupting the comparison.
  3. 3
    Watch a matched pair pulse and glowMatching icons trigger a quick scale-up pulse plus a green glow on both cards, and a small burst of colored particles fires from each card's position.
  4. 4
    Watch a mismatched pair flip backNon-matching cards stay visible slightly longer than a match (to give you time to memorize them) before automatically flipping face-down again.
  5. 5
    Track your move count and elapsed time in the HUDBoth counters update live above the grid — moves increases once per pair attempt, and the timer keeps running until the very last pair is matched.
  6. 6
    Clear the board to trigger the win celebrationMatching all pairs stops the timer, fires a larger confetti burst from the center of the grid, and shows a banner with your final move count and time.

Real-world uses

Common Use Cases

Kids' learning apps and educational games
A ready-made memory game shell for teaching apps — swap the emoji deck for vocabulary words, country flags, or math facts, and pair with a confetti celebration card styled banner for extra reward feedback.
Marketing mini-games and engagement widgets
Embed as a lightweight branded mini-game on a landing page or email campaign, swapping icons for product images or logo variants to drive time-on-page.
Reference implementation for 3D flip-card mechanics
Study the perspective/preserve-3d/backface-visibility trio alongside the 3D flip card and 3D card tilt snippets to build your own card-flip interaction from a consistent, understood pattern.
Teaching race-condition-safe UI state machines
A concrete, small example of why a "lock" flag is needed around asynchronous UI transitions — directly applicable to any interaction involving a timed pause between two user actions, not just games.
Onboarding and micro-interaction showcases
Use the pulse-glow and lightweight confetti burst as a template for celebratory micro-interactions elsewhere in a product, such as a completed checklist item or a finished onboarding step.

Got questions?

Frequently Asked Questions

Checking flippedCards.length === 2 alone is not enough, because that check happens synchronously on click while the mismatch-resolution setTimeout is still pending in the background. A player who clicks fast enough could flip a third card during the window after two mismatched cards are shown but before their flip-back timeout has fired, at which point flippedCards briefly holds an invalid third entry. Setting locked = true the instant the second card of a pair is flipped, checked at the very top of every click handler call, closes that window completely regardless of timing.

A match pause (320ms) only needs to be long enough to visually confirm the pair before the pulse-glow plays — the cards are staying flipped anyway. A mismatch pause (850ms) has to give the player enough real time to actually study and memorize both revealed icons before they flip back and hide again, since remembering card positions is the entire point of the game; a too-short mismatch pause makes the game frustrating rather than challenging.

Edit the ICONS array at the top of the script — each entry becomes one matched pair, so an 8-item array produces a 16-card (4x8 grid works well up to 6 pairs; adjust grid-template-columns in the CSS for larger sets) grid. buildDeck() automatically duplicates and shuffles whatever list you provide, so no other code needs to change.

Yes. In React, replace the module-level deck/flippedCards/locked variables with useState, and move startTimerIfNeeded's setInterval into a useEffect, storing the interval id in a ref and calling clearInterval on it in the cleanup function so a mid-game unmount does not leave a stray timer running. In Vue, hold the same state in ref()/reactive() and start/stop the interval in onMounted/onUnmounted. In Angular, manage it as component fields and clear the interval in ngOnDestroy. The Web Animations API confetti burst (Element.animate()) needs no cleanup in any framework since each animation removes its own DOM node in its onfinish callback.

The Web Animations API lets the browser's compositor drive each particle's transform and opacity animation independently and efficiently, without a JavaScript frame loop tracking dozens of particles' positions manually. Each confetti-bit is a tiny absolutely-positioned div whose entire animation (translate, rotate, fade) is described once as keyframes and handed to the browser; the onfinish callback removing the element from the DOM is the only JS involvement after spawn, keeping the effect cheap even when many bursts fire in quick succession.