A big, friendly countdown timer for small hands. Installable PWA, works fully offline, no backend and no accounts.
- Readable two ways — a huge
MM:SSreadout for readers, and a ring that visibly empties as the time runs down for pre-readers. - An animal buddy in the middle of the ring — 18 to choose from, tap it (or the Animal button) to swap. It breathes gently while counting and jumps up and down at zero. The choice persists.
- Countdown music — a soft procedural music box: a random walk through a C-major pentatonic over a I–V–vi–IV progression, so it never audibly loops even on a 20 minute timer. Synthesised rather than a media file, so it costs nothing to download and works offline. Toggle it off with the 🎵 button; the preference sticks.
- Presets + dial-your-own — quick-tap chips (1 / 5 / 10 / 20 min by default), plus a
drag-the-dial sheet with ±1m / ±10s steppers. Saved times and the last-used duration
persist in
localStorage. - A celebration, not an alarm — a rising major arpeggio with sparkles and confetti at zero, with the music ducking out first so the reward lands cleanly. The audio context is unlocked on the Start tap so iOS Safari actually sounds it.
- Honest clock — remaining time is always recomputed from a stored end timestamp, never
decremented per tick, and it resyncs on
visibilitychange/focus/pageshow. A run in progress survives a reload or the app being killed. - Screen stays awake while counting, where the Wake Lock API is available.
Live at https://swoisz.github.io/kid-timer/
iPhone / iPad — open that link in Safari (Chrome and Firefox on iOS cannot install PWAs), tap Share (□ with ↑), scroll to Add to Home Screen, then Add. It launches fullscreen with no browser chrome and keeps working with no signal.
Android — open in Chrome and use Install app from the ⋮ menu.
Tap the 🎵 Music button off and on — turning it on chirps twice. That is a two-second sound test you can run without sitting through a timer.
If you hear nothing:
- Check the ring/silent switch on the side of the phone. This is nearly always it. iOS
treats Web Audio as ambient audio by default, which the hardware switch mutes outright.
The app asks for the
playbackaudio session (navigator.audioSession) specifically to opt out of that, but it needs iOS 16.4 or newer. On older iOS the switch wins and there is no way around it from a web app. - Turn the volume up while the app is in the foreground — iOS keeps a separate volume level for media, and it can sit at zero even when the ringer is loud.
- Sound only starts after you tap GO. Browsers refuse to make noise until you have interacted with the page, which is why the audio is armed on that tap.
| Path | Purpose |
|---|---|
index.html |
The whole app — markup, styles and logic inline |
manifest.json |
PWA metadata (standalone, portrait, theme colours) |
service-worker.js |
Precaches every asset for offline use |
icons/ |
192 / 512 icons, maskable variants, apple-touch-icon |
tools/make_icons.py |
Regenerates the icons (no dependencies) |
tests/timer.test.js |
Test suite (no dependencies, no build step) |
node tests/timer.test.js77 assertions, no dependencies and no browser needed. It extracts the inline script from
index.html and runs it in a VM against a stubbed DOM, localStorage and Web Audio, driven
by a virtual clock — so it can fast-forward through a 10 minute countdown, simulate the phone
being asleep for two minutes, and assert on what the scheduler does about it.
Worth keeping green, because the two things most likely to break here are invisible in casual use: drift after backgrounding, and the music scheduler dumping a burst of catch-up notes when it falls behind the audio clock. Both are pinned by tests.
python3 -m http.server 8777
# then open http://localhost:8777/Service workers need a secure context, which localhost counts as.
After changing any cached asset, bump CACHE in service-worker.js so clients pick it up.
To regenerate the icons after editing the palette:
python3 tools/make_icons.py