webdevpuneet.com

Mash-to-Charge Button — Free Rapid-Click Meter JS Snippet

Mash-to-Charge Button · Buttons · Plain HTML, CSS & JS · Live preview

CategoryButtons

Copy & paste

Source Code

<div class="mc-wrap">
  <button type="button" class="mc-btn" id="mcBtn">
    <span class="mc-fill" id="mcFill"></span>
    <span class="mc-label" id="mcLabel">Mash to claim!</span>
  </button>
  <div class="mc-meter-track">
    <div class="mc-meter-fill" id="mcMeter"></div>
  </div>
  <p class="mc-status" id="mcStatus">Click rapidly to fill the meter before it drains</p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Discrete per-click charge combined with a continuous, delta-time-based decay loop
requestAnimationFrame drives decay using real elapsed time, not a fixed per-frame amount
Meter fires the reward exactly once, the instant it reaches 100%, then locks input
Short 120ms pulse animation per click for immediate tactile feedback
Two independently tunable constants (CHARGE_PER_CLICK, DECAY_PER_SECOND) control difficulty
Double-click default browser behavior is suppressed so rapid clicking never triggers text selection
Reset function restores the meter to zero and resumes the decay loop for a replay flow
No external animation or game-loop library required

About this UI Snippet

Mash-to-Charge Button — Rapid-Click Meter with Continuous Decay

Screenshot of the Mash-to-Charge Button snippet rendered live

A mash-to-charge button turns a single call-to-action into a small burst of gamified effort: every click adds a fixed chunk to a charge meter, but the meter also drains continuously between clicks, so only a genuinely rapid, sustained click rate can out-pace the decay and reach 100%. It is the discrete-click counterpart to a press-and-hold button — where a hold to confirm button rewards one long continuous press, this rewards rhythm and repetition, which suits hype campaigns, claim-a-reward CTAs, and playful engagement moments rather than safety-critical confirmations.

Charge added per click, decay running independently

Every click calls setMeter(charge + CHARGE_PER_CLICK), adding a fixed 9 points regardless of click speed. Separately, a requestAnimationFrame loop in frame() runs continuously from the moment the button is ready, computing real elapsed time (dt) between frames and subtracting DECAY_PER_SECOND * dt from the current charge every frame. Because the decay loop runs independent of clicks, a burst of fast clicks visibly outpaces it and the meter climbs, while pausing for even a second or two lets it visibly fall back — the meter is a genuine race between click rate and a fixed drain rate, not just a click counter with a cosmetic bar.

Why decay uses delta-time, not a fixed decrement per frame

Subtracting a flat amount per animation frame would make the drain rate depend on the device's frame rate — faster on a 144Hz display, slower on an underpowered one. Computing dt as the actual milliseconds elapsed since the last frame and scaling the decay by it (DECAY_PER_SECOND * dt) keeps the drain rate consistent in real time regardless of how often requestAnimationFrame actually fires.

A short pulse per click for tactile feedback

Each click also toggles a .pulsing class on for 120ms via setTimeout, which fades in a radial highlight behind the label through .mc-fill's opacity transition. This gives every individual click a small visual acknowledgment separate from the meter's overall progress, reinforcing the "mash faster" feedback loop the mechanic is built around.

One-shot claim, then locked

Once charge reaches 100, claim() cancels the decay loop, locks the meter at 100%, swaps the button to a solid "claimed" state, and stops responding to further clicks — the reward fires exactly once. A separate reset() function (wired to a custom mc:reset event in this demo) restarts the meter at zero and resumes the decay loop, useful for a "play again" flow.

Step by step

How to Use

  1. 1
    Click rapidly and repeatedlyEach click adds CHARGE_PER_CLICK points to the meter. A single click alone will not reach 100% before the meter decays back down.
  2. 2
    Watch the meter race the decayA continuous requestAnimationFrame loop drains DECAY_PER_SECOND points every second, so only a sustained fast click rate keeps the bar climbing.
  3. 3
    Reach 100% to claimThe instant the meter hits 100%, claim() fires: the decay loop stops, the button locks, and the reward state is shown.
  4. 4
    Feel the per-click pulseEvery click flashes a brief radial highlight behind the label for 120ms as tactile confirmation the click registered.
  5. 5
    Tune the difficultyRaise DECAY_PER_SECOND or lower CHARGE_PER_CLICK to require a faster sustained click rate; do the opposite to make it easier.
  6. 6
    Wire your reward actionPut the actual claim/unlock call inside claim(), which fires exactly once when the meter first reaches full.

Real-world uses

Common Use Cases

Gamified claim-a-reward CTAs
A playful alternative to a plain confetti button for limited-time promotions, referral unlocks, or hype-building landing page moments.
Mini-game and event landing pages
Drop this into a marketing mini-game or seasonal campaign page as a lightweight engagement mechanic that needs no backend.
Meter and decay animation pattern reference
The delta-time decay loop technique is directly reusable for any UI meter that should drain over time, such as a stamina bar, a hype meter, or a countdown-with-refill indicator.
Teaching requestAnimationFrame delta-time math
frame() is a compact, readable example of frame-rate-independent animation, useful as a stepping stone before building a full canvas game loop.
Engagement mechanic for A/B testing CTA styles
Compare click-through and completion rates of a mash-based CTA against a standard single-click button for campaigns where playful friction increases perceived reward value.

Got questions?

Frequently Asked Questions

Each click only adds a fixed CHARGE_PER_CLICK amount (9 by default), while a separate requestAnimationFrame loop continuously drains DECAY_PER_SECOND points every second regardless of whether you are clicking. A single click adds less than what decays away almost immediately afterward, so only rapid, repeated clicking can out-pace the drain.

Animation frames do not fire at a perfectly consistent rate across devices and browsers. Computing dt as the actual milliseconds elapsed since the previous frame and scaling DECAY_PER_SECOND by it keeps the drain rate consistent in real time, regardless of the device's actual frame rate.

No. claim() is only ever called from inside onClick() the instant charge first reaches 100, and it immediately cancels the decay animation frame and locks further clicks from affecting the meter, so the reward logic runs exactly once per charge cycle.

Increase DECAY_PER_SECOND to require a faster sustained click rate, or decrease CHARGE_PER_CLICK so each click contributes less — both independently tune the difficulty without touching the animation loop itself.

No, the button stays in its claimed, locked state until something calls reset() (wired to a custom mc:reset event in this demo), which zeroes the charge, removes the claimed styling, and resumes the decay loop for another attempt.

Yes. Keep charge and claimed in component state, store the requestAnimationFrame id and lastFrame timestamp in refs so updating them does not trigger re-renders, and bind the meter fill's width to the charge value. Clean up the animation frame on unmount.

Design your website