A tiny one-button mini-arcade for filling the void while a long task runs (LLMs, builds, uploads, you name it). Every game is monocolor, pure 1-bit pixel art, single canvas, zero runtime dependencies, and shares the same combo / power-up / high-score / achievement framework.
Pick a game with a single prop: <WaitingArcade game="runner" />
One button: keyboard, pointer, and touch all map to the same primary action
Tints to any colour via the color prop (defaults to currentColor )
prop (defaults to ) SSR-safe; auto-pauses when the tab is hidden
Optional localStorage -backed best score and achievements, namespaced per game
-backed best score and achievements, namespaced per game Same component still ships as <WaitingGame /> for backward compatibility
The games
Game id Mechanic Skins Jellyfish Drift jellyfish Hold to swim up, release to sink. Avoid coral & stalactites. jellyfish , octopus , paperBoat Pixel Runner runner Tap to jump, hold for higher jumps. Hop cacti, dodge birds. dino , ninja , frog Gravity Flip gravity Tap to invert gravity. Arc between floor and ceiling, dodge spikes. cube , triangle , diamond Invaders invaders Auto-fires bullets to the right. Tap to swap lane — be in the alien's lane to shoot it, out of it when it arrives. ship , fighter , saucer Rhythm Tap rhythm Notes scroll into a hit zone. Tap on the beat for short notes, hold for long ones. 3 lives. bar , dot , arrow
All five games share the same set of features: combo multiplier, near-miss bonus (or its game-specific equivalent), milestone flashes, tier ramp, screen shake, parallax background, three power-ups, and a five-achievement set. Death model varies: most games are one-hit, rhythm uses 3 lives.
Install
npm install react-waiting-game
Quick start
import { WaitingArcade } from 'react-waiting-game' ; function LoadingScreen ( ) { return < WaitingArcade game = "runner" autoStart /> ; }
Use it while waiting on an LLM
import { useState } from 'react' ; import { WaitingArcade } from 'react-waiting-game' ; function Chat ( ) { const [ loading , setLoading ] = useState ( false ) ; async function ask ( ) { setLoading ( true ) ; await fetch ( '/api/chat' , { method : 'POST' , body : '...' } ) ; setLoading ( false ) ; } return ( < div > < button onClick = { ask } disabled = { loading } > Ask </ button > { loading && ( < WaitingArcade game = "runner" autoStart persistHighScore persistAchievements /> ) } </ div > ) ; }
Controls
Every game uses the same single-button input.
Input Action Hold Space / Arrow Up / W / Touch Primary action (game-specific) Release Stop
Jellyfish — hold to thrust upward, release to sink under gravity. Walls catch you gently; only obstacles end the run.
— hold to thrust upward, release to sink under gravity. Walls catch you gently; only obstacles end the run. Runner — tap to jump, hold to jump higher (variable height). Land before the next obstacle.
— tap to jump, hold to jump higher (variable height). Land before the next obstacle. Gravity — tap to flip gravity. The player accelerates toward the active surface; flip mid-arc to thread between floor and ceiling spikes.
— tap to flip gravity. The player accelerates toward the active surface; flip mid-arc to thread between floor and ceiling spikes. Invaders — your turret auto-fires bullets to the right at a fixed cadence. Tap to swap between the upper and lower lane. Aliens that escape past you break your combo; aliens that touch you while in your lane end the run.
— your turret auto-fires bullets to the right at a fixed cadence. Tap to swap between the upper and lower lane. Aliens that escape past you break your combo; aliens that touch you while in your lane end the run. Rhythm — short tap notes scroll right-to-left into the hit zone; tap when one is centred. Long notes are hold notes — keep the button down while the bar passes through the cursor and release at the end. Missing a note, breaking a hold, or false-tapping costs one of your three lives.
<WaitingArcade /> props
Prop Type Default Description game 'jellyfish' | 'runner' | 'gravity' | 'invaders' | 'rhythm' 'jellyfish' Which mini-game to render width number 600 Canvas width in px height number 150 Canvas height in px color string 'currentColor' Single colour for everything paused boolean false Pause externally (e.g. when the LLM responds) autoStart boolean false Skip the "tap to start" prompt skin string game default Skin id; must be valid for the selected game persistHighScore boolean false Store best score in localStorage , namespaced per game storageKey string 'waiting-arcade:hi:<game>' Override key for the best score persistAchievements boolean false Store unlocked achievements in localStorage , namespaced per game achievementsStorageKey string 'waiting-arcade:ach:<game>' Override key for achievements onScoreChange (score, hi) => void — Fired when the score changes onGameOver (score) => void — Fired when the player dies onComboChange (combo, mult) => void — Fired when the multiplier changes onPickup (total) => void — Fired when pearls/coins are collected onAchievement (id) => void — Fired when a new achievement is unlocked className / style / aria-label — — Standard wrapper props
Achievements
Every game unlocks five achievements per run.
Jellyfish ( jellyfish )
ID How to earn century Reach 100 in a single run half_grand Reach 500 in a single run survivor Stay alive ~60 s pearl_diver Collect 10 pearls in a single run untouchable Pull off 5 near-misses
Runner ( runner )
ID How to earn runner_century Reach 100 in a single run runner_half_grand Reach 500 in a single run runner_survivor Stay alive ~60 s runner_coin_hoarder Collect 10 coins in a single run runner_dodger Pull off 5 near-misses
Gravity ( gravity )
ID How to earn gravity_century Reach 100 in a single run gravity_half_grand Reach 500 in a single run gravity_survivor Stay alive ~60 s gravity_collector Collect 10 coins in a single run gravity_dodger Pull off 5 near-misses
Invaders ( invaders )
ID How to earn invaders_century Reach 100 in a single run invaders_half_grand Reach 500 in a single run invaders_survivor Stay alive ~60 s invaders_sharpshooter Shoot down 10 aliens in a single run invaders_combo_master Pull off 5 close-range kills in a single run
Rhythm ( rhythm )
ID How to earn rhythm_century Reach 100 in a single run rhythm_half_grand Reach 500 in a single run rhythm_survivor Stay alive ~60 s rhythm_virtuoso Hit 25 notes in a single run rhythm_perfectionist Land 5 perfect-timing hits in a single run
Skins
< WaitingArcade game = "jellyfish" skin = "octopus" /> < WaitingArcade game = "runner" skin = "ninja" / > < WaitingArcade game = "gravity" skin = "triangle" /> < WaitingArcade game = "invaders" skin = "saucer" / > < WaitingArcade game = "rhythm" skin = "dot" />
Use GAMES[game].skins for the full list at runtime, or import the per-game constants:
import { SKIN_IDS , RUNNER_SKIN_IDS , GRAVITY_SKIN_IDS , INVADERS_SKIN_IDS , RHYTHM_SKIN_IDS , } from 'react-waiting-game' ;
Backward compatibility — <WaitingGame />
The original <WaitingGame /> jellyfish-only component still ships unchanged:
import { WaitingGame } from 'react-waiting-game' ; < WaitingGame autoStart skin = "paperBoat" persistHighScore />
It uses the original storage key waiting-game:hi , so existing high scores carry over. New projects should prefer <WaitingArcade game="jellyfish" /> .
Adding a new game
Each game is just a GameModule registered in src/games/index.ts :
import type { GameModule } from 'react-waiting-game' ; export const myGame : GameModule < MyState , MySkin , MyAch > = { id : 'mygame' , defaultWidth : 600 , defaultHeight : 150 , skins : [ 'default' ] , defaultSkin : 'default' , achievements : [ ... ] , init , tick , draw , selectScore , selectHiScore , selectPhase , selectScreenShake , selectDeathFlash , selectAchievements , selectNewAchievements , idlePrompt : 'Tap to start' , deadPrompt : 'Press to retry' , } ;
The shared useGameLoop , input bus, persistence helpers, pixel/digit drawing, screen-shake decay, and feedback types all live under src/shared/ .
Development
npm install npm test # 151 unit tests across all five game engines npm run lint # tsc --noEmit npm run build # tsup → dist/ (ESM + CJS + .d.ts)
Try the example
cd example npm install npm run dev
The example simulates an LLM call, lets you switch between games and skins live, shows combo/pickup callbacks, and lists unlocked achievements.
License
MIT