Whack-a-Mole Game — Free HTML CSS JS Snippet

Whack-a-Mole Game · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Recursive setTimeout chain (popRandomMole) produces non-metronomic, unpredictable mole timing
Single activeHole reference guarantees exactly one clickable mole and correct stale-click rejection
Randomized 650-950ms up-time and 800-1200ms spawn interval via randomBetween()
CSS transform: translate(-50%, 8%) with cubic-bezier(0.34, 1.56, 0.64, 1) bounce for the pop-up motion
.whacked squash keyframe animation (scaleX/scaleY distortion) on successful hits
30-second countdown via setInterval(tick, 1000) with automatic endGame() at zero
Best score persisted via localStorage under the whack-a-mole-best key, compared and updated each round
All timers (timerInterval, moleTimeout, hideTimeout) explicitly cleared on both startGame() and endGame() to prevent leaks

About this UI Snippet

Whack-a-Mole Game — Randomized Timers, Hit Detection, Countdown & Best-Score Persistence

Screenshot of the Whack-a-Mole Game snippet rendered live

Whack-a-Mole is a classic reflex game, and building a working version teaches a genuinely useful pattern: coordinating randomized, self-rescheduling timers with real-time user input while keeping a single source of truth for "what is clickable right now." This snippet implements the full loop in vanilla JavaScript — a 3x3 grid of holes, a mole that pops up at unpredictable intervals, a squash-hit animation on a successful click, a 30-second countdown, and a best score persisted with localStorage.

The self-rescheduling timer pattern

Rather than using setInterval on a fixed cadence, the game uses a recursive setTimeout chain via popRandomMole(). Each time a mole appears, two timers are scheduled: a hideTimeout that retracts the mole after a random 650-950ms "up" duration if it isn't clicked, and a moleTimeout that calls popRandomMole() again after a random 800-1200ms delay to bring up the next mole. This recursive-timeout approach is deliberately chosen over setInterval because each call can pick a fresh random delay — producing the unpredictable, non-metronomic rhythm that makes whack-a-mole feel alive rather than mechanical. Every timer handle (moleTimeout, hideTimeout, timerInterval) is stored in module-level variables specifically so endGame() can call clearTimeout/clearInterval on all three and guarantee no stray callback fires a mole after the round has ended.

Tracking "the one clickable mole" with a single reference

The game keeps exactly one variable, activeHole, pointing at the DOM element of whichever hole currently has its mole up. This is the crux of correct hit detection: whack(hole) only awards a point if the clicked hole strictly equals activeHole and that hole still carries the up class. Clicking an empty hole, clicking a hole whose mole already retracted, or clicking a stale reference after a new mole has appeared all fail this check safely. Before showing a new mole, popRandomMole() first retracts whatever activeHole currently is, so at most one mole is ever visible at a time, keeping the difficulty consistent and the click target unambiguous.

CSS-driven pop and squash animations

Each hole is a circular button with overflow: hidden and a radial-gradient dirt texture drawn purely in CSS — no image assets. The mole itself is an emoji positioned absolutely at the bottom of the hole with transform: translate(-50%, 100%) (fully hidden below the rim) by default. Adding the .up class transitions it to translate(-50%, 8%) using a bouncy cubic-bezier(0.34, 1.56, 0.64, 1) easing curve, which overshoots slightly for a satisfying spring-pop feel. A successful whack adds a .whacked class that triggers a squash keyframe animation — the mole briefly scales wide and flat (scaleX(1.3) scaleY(0.7)) at its midpoint before retracting, mimicking a comic "squash" impact frame.

Countdown, scoring, and persistent best score

A single setInterval ticking once per second drives the 30-second countdown, decrementing timeLeft and updating the DOM directly. When timeLeft reaches zero, endGame() stops all timers, hides any visible mole, and reveals the end screen with the final score. The best score is read from and written to localStorage under the key whack-a-mole-bestgetBest() parses the stored string with a fallback to 0, and endGame() compares the current run's score against it, writing a new value and flashing a "New best score!" message only when the record is actually broken. This mirrors the persistence pattern used in the Word Guess Game, where session state is layered on top of a small localStorage-backed record.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI coding assistant like Claude and ask it to trace exactly how the three timers — timerInterval, moleTimeout, and hideTimeout — interact, and why each one gets explicitly cleared in both startGame() and endGame() rather than just left to fire naturally. It's also worth asking the assistant to explain why activeHole is used instead of just checking the .up class alone, since that distinction is what prevents a subtle double-scoring bug. Beyond understanding, use the assistant to extend the game: ask for a difficulty curve that narrows the random timing ranges as the score climbs, a combo/streak multiplier for consecutive hits without a miss, sound effects triggered via the Web Audio API on each successful whack, or a two-player local mode. Treat the current implementation as a solid, bug-free foundation rather than a finished product — there is plenty of room to extend the scoring and difficulty systems.

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 playable whack-a-mole game in plain HTML, CSS, and JavaScript with a 3x3 grid of holes, randomized mole timing, and a countdown timer.

Requirements:
- A 3x3 grid of circular "holes"; each hole can independently show or hide a "mole" (an emoji or CSS shape) using a CSS transform-based slide-up animation, not a display toggle, so the motion is smoothly animated.
- Moles must appear at unpredictable, randomized intervals (not a fixed metronomic cadence) and each mole must automatically retract on its own after a randomized short duration if the player does not click it in time.
- Clicking a hole while its mole is visible must score a point and trigger a distinct "hit" animation (e.g. a squash/scale effect) before the mole retracts; clicking an empty hole, or a hole whose mole already retracted, must do nothing and must never score.
- At any given moment, exactly one mole should be poppable/clickable across the whole grid — showing a new mole must retract any mole that is still up elsewhere.
- Implement a 30-second visible countdown timer that starts when the player clicks "Start Game"; when it reaches zero, stop all mole spawning immediately, hide any visible mole, and show a game-over screen with the final score.
- Persist the player's best score across page reloads using localStorage, display it at all times, and clearly indicate when the just-finished round set a new best.
- Provide a "Play Again" action that fully resets score, timer, and all timers/animation state with no leftover scheduled callbacks from the previous round.
- Ensure all JavaScript timers are properly cleared when a round ends or restarts so no mole can pop up after the game has stopped.

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
    Start the roundClick "Start Game" to reset the score to 0, set the countdown to 30, and call popRandomMole() for the first time. The overlay screens are toggled with the .hidden class rather than being removed from the DOM, so restarting is instant.
  2. 2
    Whack the molesClick a hole while its mole is up (the .up class is present) to score a point. The whack() function checks that the clicked hole strictly equals the activeHole reference before awarding points, so clicking an empty or already-retracted hole is a safe no-op.
  3. 3
    Watch the timing get unpredictableEach mole stays up for a random 650-950ms (hideTimeout) and the next mole appears after a random 800-1200ms delay (moleTimeout). Both durations are recomputed on every call to popRandomMole(), so no two rounds feel identical.
  4. 4
    React to the countdownThe topbar time value decrements once per second via setInterval(tick, 1000). When it hits zero, endGame() clears every timer, hides any visible mole, and shows the end screen automatically — no player action needed.
  5. 5
    Check your best scoreAfter each round, your score is compared against the value stored under the whack-a-mole-best localStorage key. If you beat it, a "New best score!" message appears on the end screen and the stored value updates immediately.
  6. 6
    Export and restyleClick HTML or JSX to export. Swap the dirt-hole radial-gradient colors or replace the 🐹 emoji with an SVG/sprite for a different theme (whack-a-frog, whack-a-gopher). Change GAME_DURATION in the JS panel to adjust round length.

Real-world uses

Common Use Cases

Reflex-training mini-game for a game portal or landing page
Drop this into a "games" section of a portfolio or product site as a lightweight, dependency-free interactive break. Because scoring and best-score tracking are self-contained, it works as a standalone widget without any backend — just embed the component and the localStorage-backed best score persists across visits on the same device.
Teaching example for coordinating multiple JavaScript timers
This snippet is a compact, real-world demonstration of managing three concurrent timers (a repeating interval and two chained timeouts) without race conditions. It is a strong teaching artifact for explaining why setTimeout chains are sometimes preferable to setInterval when each iteration needs a different delay, and why storing timer handles in named variables is essential for reliable cleanup.
Themeable arcade-style component for seasonal promotions
Swap the mole emoji for a branded icon and change the accent colors to build a seasonal "whack the deal" promo game — e.g. click a popping icon to reveal a discount code. The squash-hit animation and countdown timer already provide the game feel; only the visual assets and the win-condition messaging need to change for a promotional reskin.
Micro-interaction case study for hit-detection state management
The activeHole pattern — a single mutable reference that gates whether a click counts — is a reusable technique for any UI where only one of many similar elements should be interactive at a time, such as a single active tooltip, a single expanded accordion panel, or a single "armed" button in a toolbar.
Starter for a difficulty-scaling or multiplayer variant
Because the spawn interval and up-time are both parameterized through randomBetween(), it is straightforward to make the game progressively harder by narrowing those ranges as the score increases, or to fork the scoring logic into a two-player local pass-and-play mode by adding a second score counter and alternating whose turn it is between hits.
Onboarding or loading-screen distraction game
Embed a short, fixed-duration version (reduce GAME_DURATION to 10-15 seconds) as an engaging distraction during a slow onboarding step, file upload, or app loading screen, similar in spirit to a Memory Match Game, giving users something interactive to do instead of staring at a spinner.

Got questions?

Frequently Asked Questions

A single module-level variable, activeHole, always points at the one hole element whose mole is currently up. Before showing a new mole, popRandomMole() first retracts whatever activeHole currently references. The whack() function only awards a point when the clicked hole is strictly equal to activeHole and still has the .up class, so clicks on any other hole — including one whose mole just retracted milliseconds earlier — are safely ignored.

setInterval fires at a fixed, unchanging cadence, which quickly becomes predictable and easy to game. This snippet instead has popRandomMole() schedule its own next call with setTimeout(popRandomMole, randomBetween(800, 1200)) — a fresh random delay is chosen every single time, so the rhythm never repeats. The same randomBetween() helper independently randomizes how long each mole stays up before auto-retracting.

The score is stored as a plain string in localStorage under the key whack-a-mole-best. getBest() reads and parses it (defaulting to 0 if nothing is stored yet), and endGame() compares the just-finished score against it, calling localStorage.setItem() only when the new score is strictly higher. Because localStorage is scoped per-origin and persists indefinitely, the best score survives page reloads and browser restarts on the same device and browser.

Yes. Change the GAME_DURATION constant at the top of the JS panel to lengthen or shorten the countdown. To increase difficulty, narrow the ranges passed to randomBetween() inside popRandomMole() — for example reducing the up-time range from 650-950ms to 400-600ms makes moles disappear faster, and reducing the spawn-delay range makes moles appear more frequently.

No. The moment whack() awards a point it immediately sets activeHole to null and removes the .up class from that hole, so a second click on the same hole in the same frame fails the equality check in whack() and is a no-op. Only one point can be scored per mole appearance.