A portable trainer and speed test. 62 position-based lessons, 9 keyboard layouts, seeded races you can share, and timing accurate to the keystroke. One container, no build step, no network, no account.
The app opens on two tabs. One teaches, one measures. Both share the same engine, so the numbers mean the same thing in each.
62 lessons across 9 units: home row, top row, bottom row, shift, punctuation, digits, symbols, fluency, and one called extra hard. Two new keys at a time, accuracy gated before speed.
15/30/60/120 seconds, 10 to 100 words, short to long quotes, or your own text. Punctuation and numbers optional. Every run is seeded.
Per-key error rates and latency build up across runs into a heatmap, feed the adaptive weak-key lesson, and drive your personal bests.
The image is nginx plus a folder of static files. Nothing to compile, nothing to install, no runtime dependency.
# clone and start
git clone https://github.com/bhagesh-h/tt.git
cd tt
docker compose up -d
Then open localhost:8080. Change the port with TT_PORT=9000 docker compose up -d.
# without compose
docker build -t tt .
docker run --rm -p 8080:80 tt
No Docker? Every script is a plain classic script with relative paths, so
app/index.html opens straight off disk. Some browsers refuse
localStorage on file://; when that
happens tt keeps your settings in memory for the session and says so in the settings window.
Just start typing. The clock begins on your first key, not on a countdown. There is no start button and nothing to click before you begin.
| key | does |
|---|---|
| tab | restart the current text |
| esc | new text, new seed unless you pinned one |
| enter | on the results screen: next run |
| r | on the results screen: repeat the same text |
| ctrl + k | show or hide the on-screen keyboard |
| ctrl + , | settings |
| ctrl + backspace | delete the whole word |
Every run has a seed. Leave the box empty for a fresh one each time, or type one to pin it. copy challenge on the results screen gives you a link that reproduces the exact words:
https://bhagesh-h.github.io/tt/app/#m=time&v=30&s=k7q2mp&l=english1k&p=0&n=0
Anyone opening it types the same words in the same order. Test recipes deliberately exclude the keyboard layout, so a QWERTY player and a Colemak player race exactly the same text.
Lessons name a physical key, never a character. That one decision means the whole course works on every layout without a second tree, and the finger being trained is the same in every column below.
| lesson | QWERTY | Dvorak | Colemak | QWERTZ |
|---|---|---|---|---|
| index anchors | f j | u h | t n | f j |
| ring fingers | s l | o n | r i | s l |
| middle fingers | d k | e t | s e | d k |
flask and gash, not fdjk sldg forever.Typing fast at 88% accuracy is not a skill worth rewarding. Someone still has to re-read and fix the text. So accuracy is cubed, and the score is built on top of that.
| accuracy | multiplier | 100 wpm becomes |
|---|---|---|
| 100% | 1.000 | 100 |
| 98% | 0.941 | 94 |
| 95% | 0.857 | 86 |
| 90% | 0.729 | 73 |
| 85% | 0.614 | 61 |
100 × (1 − tanh(cv + cv³/3 + cv⁵/5)). Small wobble barely costs; stop-start typing collapses it.Every reported number comes from one artefact: the keystroke log. Nothing is sampled live, so nothing depends on frame rate or on how busy the browser was.
KeyboardEvent.timeStamp is when the key actually fired, not when JavaScript got round to it. Implausible values fall back to performance.now().now − t₀ − paused.Six themes, red green and blue in dark and light, plus one you build yourself. The keyboard stays a plain monochrome layout display with shallow 3D keycaps, so it never competes with the text above it.
Practising a new layout? Turn on remap input and tt reads the physical key you pressed and translates it through the layout you picked, so there is no need to change your operating system's keyboard settings.