Tap the letters. Sound out the words. Collect the stars!
A gamified phonics and reading game for kids (ages 3-6) — built to explore modern full-stack TypeScript architecture with a focus on clean patterns, animation, and delightful UX. Not intended for production use.
- Letter-by-letter sounding: Tap colorful tiles or use arrow keys to activate each letter left-to-right, with rising-pitch audio feedback
- 10 progressive levels: From 2-letter words (
at,it) to 5-letter blends and digraphs (smile,crane) - Connected word animation: All letters activate → tiles unify in color, a glowing border wraps them, and the moon mascot celebrates
- Fill-up progress button: Fills as letters are selected, turns green and bounces when complete
- Animated mascot: Inline SVG crescent moon with expressions — idle float, happy shake, celebrating spin with sparkles
- Star rewards & confetti: Each word earns a star; completing 8 words triggers a level-complete celebration with particle effects
- Web Audio sounds: Synthesized tones for letter activation, word completion, and level celebration — no audio files
- Persistent progress: Levels stay unlocked across refreshes via localStorage
- Bun >= 1.0
git clone https://github.com/yourusername/moonbeam.git
cd moonbeam
bun install
bun devOpen http://localhost:3000.
bun run test # run all 50 unit tests (Vitest + jsdom)
bun run test:watch # run tests in watch mode
bun run test:ui # open Vitest UI in browser
bun run type-check # TypeScript strict check
bun run lint # ESLint
bun run build # production build| Concern | Choice | Why |
|---|---|---|
| Framework | Next.js 16 App Router | Minimal server component shell, client-side game logic |
| Animation | Motion (framer-motion v12) | Spring physics for tile bounces, staggered reveals, mascot expressions |
| Styling | Tailwind CSS v4 | CSS-based @theme inline config, no config file to maintain |
| Audio | Web Audio API | Synthesized tones — zero audio file downloads |
| Persistence | localStorage | Zero server-side state, statically exportable |
| Font | Fredoka (Google Fonts) | Rounded, variable-weight, designed for children's readability |
| Testing | Vitest + Testing Library | Jest-compatible API, Vite-native speed |
Home (Server Component)
→ Play link → /play (Server Component shell)
→ GameShell (Client Component, useReducer)
→ LevelSelect | WordDisplay | LevelComplete (screen routing)
User taps a letter tile
→ ACTIVATE_NEXT action → reducer updates activatedCount
→ playLetterSound(index) → Web Audio tone
→ LetterTile animates to active state
All letters activated
→ tiles unify in color, border wraps word
→ "I got it!" button bounces → user taps
→ playWordComplete() → NEXT_WORD action
→ 8/8 words → LEVEL_COMPLETE → celebration + confetti
Progress saved to localStorage on level completion
moonbeam/
├── app/
│ ├── layout.tsx # Root: Fredoka font, metadata
│ ├── page.tsx # Home screen (server component)
│ ├── globals.css # Tailwind v4 theme, keyframes
│ └── play/
│ └── page.tsx # Thin wrapper → GameShell
├── components/
│ ├── GameShell.tsx # Orchestrator (useReducer + localStorage sync)
│ ├── LetterTile.tsx # Animated letter button (Motion spring)
│ ├── WordDisplay.tsx # Row of tiles + keyboard handler + fill button
│ ├── LevelSelect.tsx # 10-level grid with colored badges
│ ├── LevelComplete.tsx # Celebration overlay with confetti
│ ├── ProgressBar.tsx # Star progress in frosted pill
│ ├── Mascot.tsx # Moon SVG with mood expressions
│ └── StarBurst.tsx # Confetti particle effect
├── lib/
│ ├── types.ts # All types, constants, color palettes
│ ├── words.ts # Static word data (10 levels, 8 words each)
│ ├── progress.ts # localStorage read/write with validation
│ ├── sounds.ts # Web Audio synthesizer (letter, word, level sounds)
│ └── utils.ts # cn() utility
└── __tests__/ # Mirrors source structure
├── lib/ # words, progress
└── components/ # LetterTile, WordDisplay, LevelSelect, ProgressBar, GameShell
useReducer over useState. Game state has multiple interdependent fields (level, word index, activation count, completion set). A pure reducer function keeps transitions atomic and testable outside React.
No database. Word lists are static data. Progress lives in localStorage. Zero server-side state means the entire app can be statically exported.
Motion for complex animations. Spring physics for tile bounces, staggered star reveals, and mascot expressions. CSS handles simpler transitions (hover states, gap transitions).
Web Audio over audio files. Synthesized tones using the Web Audio API — rising C-major scale for letter sounds, ascending arpeggios for word completion, and a sparkly melody for level celebration. Zero file downloads, instant playback.
Inline styles for dynamic colors. Tailwind can't detect dynamically interpolated class names (bg-${var}), so tile colors use inline style={{ backgroundColor }} with hex values from a constant array.
White background, no dark mode. This is a kids' app — a clean white background with vibrant pastel tiles is always appropriate. No theme switching complexity.
Fredoka font. A rounded, variable-weight Google Font specifically designed for readability in children's contexts.
- Pronunciation audio clips per letter using Web Speech API
- Drag-to-reorder mode for scrambled words
- Parent dashboard with progress tracking charts
- Haptic feedback on mobile via Vibration API
- PWA support for offline play