A metronome seems simple. Play a click at regular intervals. How hard could it be?
Harder than you'd think. I wanted to build a metronome that musicians could actually practice with—something that wouldn't drift, wouldn't stutter, and would feel as solid as a hardware unit. Here's what I learned.
The Problem with setInterval
Your first instinct is probably setInterval. Set it to fire every 500ms for 120 BPM. Done, right?
Wrong. JavaScript timers aren't precise. They guarantee a minimum delay, not an exact one. If the main thread is busy—rendering, handling events, garbage collecting—your timer callback gets delayed. Over a few minutes, you'll drift noticeably. Musicians will feel it.
The Solution: Recursive setTimeout
Instead of setInterval, I use recursive setTimeout. Each tick schedules the next one by reading the current BPM from a ref. This means tempo changes take effect immediately—no pause, no restart, no stutter.
const scheduleTick = () => {
if (!isPlayingRef.current) return;
playClick(isAccent);
// Read fresh BPM on every tick
const msPerBeat = 60000 / bpmRef.current;
setTimeout(scheduleTick, msPerBeat);
};The Web Audio API handles the actual sound with sample-accurate precision. JavaScript just triggers it.
Two Click Sounds
I synthesize two different click sounds:
- • Click: Sharp, Ableton-style. Square wave through a highpass filter, fast decay.
- • Beep: Pure sine wave. Classic metronome sound, gentler on the ears.
Both are synthesized—no samples. The accent (beat 1) uses a higher pitch to stand out. You can switch between them with one click.
Italian Tempo Markings
Musicians don't always think in BPM. They think in Andante, Allegro,Presto. So I added Italian tempo markings that update in real-time as you adjust the tempo:
- • Largo (40–59): Slow and broad
- • Adagio (66–76): Slow and stately
- • Andante (77–107): Walking pace
- • Moderato (108–119): Moderate
- • Allegro (130–155): Fast, bright
- • Vivace (156–175): Lively
- • Presto (176–199): Very fast
These are hidden by default to keep the interface minimal. Click "More" to expand them, along with preset buttons for common tempos.
Drag-to-Adjust Knobs
Sliders feel wrong for a metronome. I implemented Ableton-style knobs: drag up to increase, drag down to decrease. A popup shows the current value while you're adjusting, then fades away.
The tempo knob goes from 40 to 240 BPM. The volume knob controls the click level. Both update in real-time without restarting the metronome.
Time Signatures
Not every piece is in 4/4. The metronome supports 2/4, 3/4, 4/4, 5/4, 6/8, and 7/8. The beat indicator adjusts accordingly—if you're in 7/8, you see 7 boxes light up in sequence.
The first beat (downbeat) is always visually distinct, regardless of time signature.
Keyboard Shortcuts
Musicians' hands are often busy:
- • Space: Start/stop
- • T: Tap tempo
Tap tempo averages your last 8 taps to calculate BPM. You can tap on your desk, and the metronome will match.
The Hardware Feel
The whole interface is enclosed in a border, like a physical device. The beat indicator shows numbered boxes that light up in sequence. The large BPM display pulses slightly on the downbeat.
It's minimal by default—just the essentials. But it's also expandable when you want the Italian markings or quick-access presets.
Try it at /projects/bpm-counter. It runs entirely in your browser. No signup, no installation—just a metronome that works.