webdevpuneet.com

Rhythm Tap Game – Free HTML, CSS & JS Snippet with Combos

Rhythm Tap Game · Games · Plain HTML, CSS & JS · Live preview

CategoryGames

For supporters

Source Code

Want the source code?

Play the game for free. Become a WebDevPuneet supporter to unlock the complete HTML, CSS and JavaScript source code, the exports (HTML, React, Vue, Angular, Tailwind) and Fork.

Play freeSource code: Patreon

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

Delta-time note movement via requestAnimationFrame, keeping fall speed constant across frame rates
Nearest-note matching in laneKeyDown(): scans all unhit notes in a lane and judges the closest one by its center
Two-tier timing windows (PERFECT_WINDOW and GOOD_WINDOW) producing graded Perfect, Good and Miss judgments
Combo multiplier up to x4 with a bump animation, stage glow that heats up and milestone callouts
Web Audio synth: each lane plays a note of a C major chord, so hits build a melody with no audio files
Difficulty ramp that speeds up notes, shortens gaps and adds two-note chords later in the run
Colour-coded lanes, lane glow on press, particle sparks on every hit and a screen shake on a miss
3-2-1-GO countdown, a progress bar, a mute button and an S to D rank on the results screen
Best combo saved to localStorage inside a try/catch, so it also works in sandboxed iframes
Keyboard (D/F/J/K) and pointer input, with Ctrl/Cmd shortcuts and key repeat ignored

About this UI Snippet

Rhythm Tap Game — Falling Notes, Combo Multipliers, Synth Sound & Rank Grades

Screenshot of the Rhythm Tap Game snippet rendered live

Rhythm games boil down to one core mechanic done well: judging how close a player's input is to a moving target in time, then turning that closeness into a graded, satisfying result. This snippet is a four-lane falling-note rhythm game with no audio files and no libraries: requestAnimationFrame, delta-time movement, a nearest-note matching algorithm, a tiny Web Audio synth and a handful of CSS animations that make every hit feel good.

Falling notes with a rising difficulty curve

Every note is a small div inside its lane's track, advanced each frame by speed * dt, where dt is the real elapsed time since the previous frame. This is the same delta-time approach as the Flap & Dodge Obstacle Game, so fall speed is constant on any refresh rate. Both speed and the gap between spawns are interpolated from the share of notes already spawned, so a 40-note run starts gentle and ends fast. After the first quarter of the run, a spawn can also drop a second note in a different lane (a chord), and pickLane() never allows three notes in a row on the same lane.

Judging a hit: nearest note, graded by distance

When a lane is pressed, laneKeyDown() scans every unhit note in that lane and picks the one whose center is closest to the center of the hit line. Within GOOD_WINDOW it counts as a hit, and within the tighter PERFECT_WINDOW it becomes a Perfect. A note that slides past the window unhit is registered as a Miss on the next frame by the game loop. Pressing a lane with no note nearby is a harmless air tap.

Combo multiplier, heat and milestones

Every consecutive hit raises the combo, and each 10 hits raises the multiplier (up to x4), which scales the 100 or 50 points a Perfect or Good is worth. The multiplier badge bumps when it increases, the stage glow changes colour through a data-heat attribute, and combos of 10, 20, 30 and 40 trigger a callout and a short arpeggio. A Miss resets the combo, drops the multiplier back to x1 and shakes the stage.

Sound and juice, no assets required

Each lane is tuned to a note of a C major chord (C4, E4, G4, C5), and tone() plays it through an OscillatorNode and a GainNode with a quick decay, so every hit adds to a melody. Perfect hits add an octave sparkle, misses play a falling sawtooth, and a mute button turns everything off. Visual feedback comes from lane colours, a glow on the pressed lane, particle sparks created and removed per hit, and a 3-2-1-GO countdown.

Ending the run and ranking the player

The run ends when all 40 notes have been spawned and resolved. gradeFor() turns accuracy and the share of Perfects into an S, A, B, C or D rank, shown with the score, Perfect/Good/Miss breakdown and best combo. The best combo is saved to localStorage (inside a try/catch so it also works in sandboxed iframes), and a "New best combo!" line appears when it was beaten.

Step by step

How to Use

  1. 1
    Start a runClick "Start". A 3-2-1-GO countdown plays, then notes begin falling toward the hit line in four colour-coded lanes.
  2. 2
    Play the lanesThe lanes are mapped to D, F, J and K, shown as key caps at the bottom. On a touch screen, tap the lane itself. Each lane plays its own note when you hit it, so you make the music.
  3. 3
    Time your hitsPress the lane as a note crosses the hit line. laneKeyDown() finds the closest unhit note in that lane and grades it Perfect or Good, with sparks and a flash on the lane.
  4. 4
    Build the multiplierConsecutive hits grow your combo. Every 10 hits raises the multiplier up to x4, the stage glow gets hotter and milestone callouts like "On fire!" appear.
  5. 5
    Survive the ramp-upNotes get faster and closer together as the run progresses, and chords (two notes at once) start appearing. A missed note resets your combo and multiplier and shakes the stage.
  6. 6
    Check your rankAfter all 40 notes, you get an S to D rank, your Perfect/Good/Miss breakdown, accuracy and best combo, which is saved with localStorage.

Real-world uses

Common Use Cases

Teaching timing-window and nearest-match input judging
The core challenge in any rhythm or timing game is converting an input event into a graded result based on proximity to a target. The nearest-note search followed by a two-tier distance check is a clean, minimal example of that pattern, useful for any interaction that needs "how close was that" instead of a yes or no.
Portfolio piece showing real-time input, audio and game feel
A working four-lane game with graded accuracy, combo multipliers, synthesized sound and particle effects shows state management, precise input handling and polish in one self-contained piece, which goes well beyond a static UI mockup.
Quick, replayable diversion for a personal site
A 40-note run takes about a minute, ends with a rank, and invites another try to beat your best combo. That makes it a good fun corner for a personal site, a 404 page or a loading screen.
Reference for game-feel effects in plain CSS and JS
The restartable CSS animations, per-lane colour variables, spark particles, screen shake and heat-based glow are reusable techniques for any reaction or timing mini-game, or for onboarding flows that need to celebrate a well-timed action.
Base for audio-synced or difficulty-scaling variants
Notes are spawned from a simple interval, so this is a good starting point for syncing spawns to the timestamps of a real track with the Web Audio API clock, or for adding difficulty modes, as you could also do with the Flap & Dodge Obstacle Game.
Reaction-time and hand-eye coordination practice
With accuracy percentages, ranks and a saved best combo, the game gives users a concrete, improvable score across repeated sessions, which suits an educational or wellness app.
Related: Sorting Swap Puzzle Game
See the Sorting Swap Puzzle Game for a related games pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

laneKeyDown() finds the closest unhit note in the pressed lane by comparing the center of each note with the center of the hit line. Within PERFECT_WINDOW (16px) it is a Perfect worth 100 points times the multiplier, within GOOD_WINDOW (34px) it is a Good worth 50 times the multiplier. If no note in that lane is inside GOOD_WINDOW, the press is an air tap with no penalty.

A Miss is only registered when a note slides past the hit line unhit, checked every frame in the game loop. Pressing a lane with no note in range just plays a quiet note, which keeps the lanes feeling like an instrument and avoids punishing exploratory taps.

tone() creates an OscillatorNode and a GainNode with the Web Audio API, sets a frequency, fades the gain to near zero and stops the oscillator. Each lane has its own frequency from LANE_FREQS (C4, E4, G4 and C5). The AudioContext is created inside the Start click so browsers allow sound, and the mute button simply makes tone() return early.

Each frame the loop computes t as the share of notes already spawned, then interpolates speed between BASE_SPEED and MAX_SPEED and the spawn gap between BASE_INTERVAL and MIN_INTERVAL. Change those four constants, or TOTAL_NOTES, to make the run easier, harder or longer. Chords are added in spawnStep() once t passes 0.25.

When a new record is reached, saveBest() writes it to localStorage under rhythm-tap-best-combo. Accessing localStorage throws a SecurityError in sandboxed iframes and some private modes, so both loadBest() and saveBest() are wrapped in try/catch. If storage is blocked, the game still runs and just does not remember the best combo.

Yes. Add a key to LANE_KEYS, a colour to LANE_COLORS and a frequency to LANE_FREQS, then duplicate a .rt-lane block in the HTML with the next track id, a --c colour and a key label, and extend the tracks array. Replace the 4 in pickLane() and spawnStep() with your lane count.