Skip to content

Repository files navigation

Study Timer

Study Timer

A calm, offline-ready study timer for kids with modes, a live countdown, and confetti rewards

Live · Repo · Portfolio

Study Timer - preview

Study Timer

A calm, responsive study timer for kids. Pick a study mode, run the circular countdown, and watch it lock with a checkmark and a burst of confetti once the session is complete. No accounts, no backend - everything runs in the browser and remembers itself between visits.

Study Timer in dark mode with the Reading mode selected

License: MITReactTypeScriptViteTailwind CSSPWATests

Contents

Features

  • Seven study modes - Reading, Writing, Math, Puzzle, Art, Music, and Game - each with its own color and Lucide icon. Music and Game start hidden; toggle them on in Settings.
  • Flat-glass mode selector - a row of frosted-glass icon buttons with the active one enlarged and glowing.
  • Wall-clock-anchored countdown that stays accurate across background-tab throttling and re-syncs the moment you return to the tab.
  • Session locking - a mode locks with a checkmark once its session completes and cannot be re-run until the duration changes or sessions are reset.
  • Celebration effects - confetti and a two-tone WebAudio chime on completion, both skipped when the OS requests reduced motion.
  • Dark and light themes, toggled from Settings and remembered between visits.
  • Configurable session length - 5s (test), 1 minute, 5 minutes, or 15 minutes.
  • Editable name shown in the header, set at build time or edited live in Settings.
  • Installable PWA that works fully offline after the first load.
  • Fully responsive from iPhone SE to iPad Pro, with a custom lg-land breakpoint that switches from a stacked to a side-by-side layout - and no page scrolling in any size.
  • Persistent by default - name, theme, duration, enabled modes, and completed sessions all live in localStorage and restore on load.

Architecture

Study Timer is a single client-side React app - there is no server, no database, and no network calls after the assets load. App.tsx owns all state through a set of localStorage-backed hooks and composes a handful of presentational components. A service worker (vite-plugin-pwa) precaches the build so the app keeps working offline.

flowchart LR
subgraph Browser["Browser (client only)"]
direction TB
App["App.tsx<br/>state + orchestration"]
Hooks["hooks/<br/>useCountdown · useLocalStorage"]
Lib["lib/<br/>constants · theme · audio"]
UI["components/<br/>ModeSelector · TimerCircle<br/>SettingsModal · Confetti · Ripple"]
SW["Service worker<br/>(vite-plugin-pwa)"]
end
Store[("localStorage<br/>name · theme · duration<br/>modes · completed")]
App --> Hooks
App --> UI
App --> Lib
Hooks -- "read / write" --> Store
SW -- "precache assets" --> App
Loading

One direction of flow, three layers:

LayerFilesRole
ShellApp.tsx, main.tsxHolds all state, wires hooks to components, formats the header clock
Hookshooks/useCountdown.ts, hooks/useLocalStorage.tsWall-clock countdown engine; persistent state primitive
Liblib/constants.ts, lib/theme.ts, lib/audio.tsModes + storage keys; glassmorphism styles; WebAudio chime
UIcomponents/*Mode selector, timer ring, settings modal, confetti, ripple layer
StatelocalStorageName, theme, duration, enabled modes, completed sessions

The countdown never trusts a raw setInterval tick count. Each tick recomputes timeLeft from endTime - Date.now() and re-syncs on visibilitychange, so a throttled or backgrounded tab still shows the true remaining time.

How a session runs

sequenceDiagram
participant K as Kid
participant S as ModeSelector
participant A as App.tsx
participant C as useCountdown
participant F as Confetti + audio
K->>S: pick a study mode
S->>A: setActiveMode(id)
A->>C: reset to duration (mode id = resetToken)
K->>C: tap the ring to start
C->>C: each tick = endTime - Date.now()
C-->>A: onComplete when timeLeft hits 0
A->>A: mark mode completed (localStorage)
A->>F: confetti + two-tone chime
A-->>K: ring locks with a checkmark
Loading

Design decisions and trade-offs

Study Timer optimizes for one thing: a distraction-free timer a young kid can run alone on a tablet, that survives a refresh and a dead network. Every choice below follows from that.

DecisionChosenAlternativeWhy this trade-offCost we accept
PersistencelocalStorageBackend + accountsZero setup, instant, private to the deviceState is per-device; nothing syncs across devices
CountdownWall-clock (Date.now())setInterval tick countAccurate under background-tab throttling; re-syncs on focusNeeds a visibilitychange re-sync path
BackendNone (static SPA)API serverNothing to run or secure; deploys anywhere as static filesNo shared state, no server-side features
OfflineService worker precacheOnline-onlyWorks on a tablet with no wifi after first loadMust ship and version a service worker
SoundWebAudio, one shared contextBundled audio filesNo asset to load; a single reused AudioContext avoids the browser context capSynthesized tone, not a recorded sample
StylingTailwind + inline dynamic stylesCSS modulesFast to build; per-mode colors set at runtimeSome inline style for dynamic color values
MotionRespects prefers-reduced-motionAlways animateConfetti is skipped for users who ask for less motionA branch on every celebration

Tech stack

  • React 19 + TypeScript (strict) - UI and state
  • Vite 7 - dev server and build
  • Tailwind CSS v3 - styling
  • vite-plugin-pwa (Workbox) - installable, offline-ready PWA
  • Lucide React - icons
  • Web Audio API - completion chime
  • Vitest + React Testing Library - unit and component tests
  • localStorage - persistence

Quick start

git clone https://github.com/bunlongheng/study-timer.git
cd study-timer
npm install
npm run dev

The dev server runs at http://localhost:3019. Build a production bundle with npm run build and preview it with npm run preview.

ScriptWhat it does
npm run devStart the Vite dev server on port 3019
npm run buildType-check, then build the production bundle
npm run previewServe the production build locally
npm run lintRun ESLint
npm testRun the Vitest suite once
npm run test:watchRun Vitest in watch mode

Configuration

No environment variables are required. One optional build-time variable sets the default name shown in the header (a kid can always change it live in Settings):

Env varDefaultPurpose
VITE_USER_NAMENorden HengDefault header name baked into the build; overridden by any name saved in Settings

Project layout

study-timer/
├── index.html # App shell + PWA meta
├── public/
│ ├── favicon.ico
│ └── icons/ # PWA / apple-touch icons
├── src/
│ ├── App.tsx # State + orchestration
│ ├── main.tsx # React entry
│ ├── index.css # Tailwind layers + custom styles
│ ├── components/
│ │ ├── ModeSelector.tsx # Frosted-glass mode buttons
│ │ ├── TimerCircle.tsx # SVG progress ring
│ │ ├── MillisecondDisplay.tsx
│ │ ├── SettingsModal.tsx # Theme, duration, name, modes, reset
│ │ ├── Confetti.tsx # Completion confetti
│ │ ├── RippleLayer.tsx # Tap ripples
│ │ └── ToggleSwitch.tsx
│ ├── hooks/
│ │ ├── useCountdown.ts # Wall-clock countdown engine
│ │ └── useLocalStorage.ts # Persistent state primitive
│ └── lib/
│ ├── constants.ts # Modes, storage keys, defaults
│ ├── theme.ts # Glassmorphism styles
│ └── audio.ts # WebAudio completion chime
├── docs/screenshots/ # README images
├── vite.config.js # Vite + PWA config
└── vercel.json

License

MIT (c) Bunlong Heng


Built by Bunlong Heng · See it in my portfolio →

About

A calm, offline-ready study timer for kids - circular countdown, seven study modes, confetti on completion. React + TypeScript PWA, no backend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages