____ _ _____ _
/ ___| |__ _ __ ___ _ __ ___ |_ _(_)_ __ ___ ___ _ __
| | | '_ \| '__/ _ \| '_ \ / _ \ | | | | '_ ` _ \ / _ \ '__|
| |___| | | | | | (_) | | | | (_) | | | | | | | | | | __/ |
\____|_| |_|_| \___/|_| |_|\___/ |_| |_|_| |_| |_|\___|_|
In a world saturated with complex, data-harvesting productivity applications, there is a distinct beauty in pure, client-side tools that do one thing and do it exceptionally well. The ChronoTimer Utility was born out of a simple frustration: most web-based timers and stopwatches are either visually outdated or bloated with tracking scripts, bulky frameworks, and server dependencies. We wanted to build a clockwork-precise utility that executes entirely in the user's browser, demands zero network requests, loads instantly, and delivers a premium, high-fidelity aesthetic. By stripping away heavy frameworks, we set out to prove that vanilla web technologies (HTML5, CSS3, and JavaScript) can be crafted into a sophisticated, hardware-accelerated productivity interface.
This system was built as a single-person project assigned during an intensive internship in Web Application Development at Prodigy Infotech by Godfrey. Tasked with upgrading a rudimentary stopwatch into a state-of-the-art multi-tab chronometer, Godfrey drove the design, frontend engineering, sound synthesis architecture, and user experience updates. This project serves as a capstone demonstrating how close-to-metal browser APIs can be leveraged to build responsive, performant, and visual-first software.
Modern browsers are notoriously hostile to high-precision timer scripts running in the background. To conserve CPU cycles and battery life, browser engines aggressively throttle inactive tabs, restricting setInterval and setTimeout executions to once per secondβor pausing them entirely. For a stopwatch displaying centiseconds, or a Pomodoro focus tracker requiring minute-by-minute updates, simple timer loops will drift, delay, and ultimately fail the user.
Our primary challenges were:
- Mathematical Accuracy: Maintaining sub-millisecond precision even when the browser tab is minimized or backgrounded.
- Offline-first Audio: Delivering rich, custom sound notifications (ticks, click feedback, alarms) without requesting heavy MP3/WAV assets that fail under poor network conditions or raise cross-origin policies.
- Cross-Device Usability: Rendering circular SVG dashboards and data tables that adjust to screens ranging from a compact 320px mobile viewport to a high-density 4K monitor, while maintaining fluid 60fps animations.
We reject the basic, uninspired gray-and-blue layouts of typical utilities. Our design language is anchored in Premium Dark Glassmorphism. We treat the screen as an illuminated physical surface where panels hover over floating, glowing background gas clouds. Key design pillars include:
- Visual Hierarchy through Translucency: Using high-blur glass filters (
backdrop-filter: blur(25px)) to keep focus on the active dashboard. - Typographical Stability: Leveraging Google Outfit for UI controls and JetBrains Mono for timer digits. Timer numbers must use monospaced, tabular-num formats so that ticking digits do not cause the layout to jump or expand.
- Intentional Lighting: Implementing soft neon glow states (cyan for stopwatch, emerald green for break timers, and coral orange for focus sessions) that immediately communicate the application state through ambient color changes.
The development process progressed through three major phases:
- Phase 1: Precision Re-engineering: We moved from incrementing a simple counter variable to tracking time using monotonic delta calculations (
performance.now()). The execution loop was rebuilt aroundrequestAnimationFrame, aligning screen updates directly with the hardware refresh cycle. - Phase 2: Sound Synthesizer: We integrated the browser's native Web Audio API, coding a clean synthesizer class that creates, connects, and releases audio nodes on-the-fly to play custom frequencies and sound sweeps.
- Phase 3: Adaptive Refactoring: We stripped out custom scrollbars in favor of a clean scrollbar-free system, built custom mobile queries to transition modals into touch-friendly bottom sheets, and structured responsive layouts using CSS Grid and flex models.
Security is often neglected in utility applications. We designed the ChronoTimer Utility with a zero-trust footprint:
- Zero External Calls: No scripts are loaded from third-party CDNs (aside from official Google Fonts). There are no tracking pixels, analytics suites, or remote APIs.
- No Database Storage: All states, including Pomodoro durations and volume configurations, are persisted strictly in the browserβs sandboxed
localStorage. No data ever leaves the user's local machine. - Content Security Policy Friendly: By avoiding inline styles in production and eliminating
eval(), the code conforms to strict security headers.
Modern web browsers block audio playback until a user explicitly interacts with the document (clicking a button, tapping a tab). Programmatic alarms or sound indicators triggered without interaction will fail, throwing console warnings. To handle this elegantly:
- We initialize the browser's
AudioContextlazily. - On the very first user mouse-down or key-press on the app, the code invokes the audio activation sequence.
- If the context is suspended, we call
.resume()to restore state. This ensures all transition click bleeps, ticks, and completion alarms sound reliably without blocking the UI or throwing runtime execution errors.
During usability testing, we observed that high-frequency productivity users navigate primarily via keyboard commands. Relying purely on mouse clicks slows down focus logging and split lap tracking. We implemented physical key mapping shortcuts (Space to toggle, L to lap, R to reset, and T to cycle tabs).
For mobile layouts, we replaced typical center-aligned desktop modals with bottom-drawer sheets. This mirrors native mobile design guidelines, placing control interfaces closer to the user's thumb for quick interactions.
- The Background Throttling Solution: When the browser background throttles the tick rate, we calculate the exact delta time upon tab reactivation. The clock instantly catches up to the real elapsed time. By using Web Audio's scheduler, completion alarms are scheduled on the audio hardware thread, ensuring sounds play on time even if the main JS thread is throttled.
- CSV Export Without Servers: Building a spreadsheet exporter completely client-side. We construct a CSV string buffer dynamically, encode it into a data URI, and mount a temporary anchor node (
<a>) to trigger a native download block.
This utility was built, tested, and iterated in close collaboration with beta-testers. Testing occurred across multiple browsers (Chrome, Safari, Firefox, Edge) and platforms (Windows, macOS, iOS, Android). Feedback on button responsiveness led to adding hardware-accelerated CSS properties (will-change: transform and translate3d), eliminating micro-stutters during heavy animations on older mobile devices.
We learned that vanilla HTML5/CSS3 APIs are extremely powerful and often perform better than bulky modern framework bundles. Hand-coding the circular progress offsets, responsive bottom drawers, and Web Audio synthesizers proved that keeping the dependency footprint minimal results in a faster, more maintainable, and highly secure codebase.
The future roadmap for the ChronoTimer Utility includes:
- Offline Service Worker Sync: Packaging the app as a Progressive Web App (PWA) so it can install and run offline.
- Multi-Timer Stacking: Allowing users to run multiple countdown timers in parallel.
- Webhooks Integration: Triggering local HTTP requests when Pomodoro focus rounds complete, allowing users to integrate the timer with smart-home lights or custom scripts.
The name ChronoTimer combines Chronos (the personification of time in Greek mythology) with Timer (representing mathematical precision and utility). It reflects our mission to build a premium, visual-first timekeeping tool.
"I hope you enjoy using ChronoTimer as much as I enjoyed building it. In a world full of complex apps, I hope this clean, precise, and visual-first utility serves as a reliable partner in your daily focus workflow."
β Godfrey
- Project Overview
- Key Features
- System Architecture & Diagrams
- Directory Structure
- Local Installation & Quickstart
- HTML Structural Layout Directory
- CSS Styling & Design System Details
- Core ES6 JavaScript Logic Deep Dive
- Web Audio Synthesis Engine
- Keyboard Shortcuts & Hotkey Architecture
- Configuration & Customization Reference
- Browser Compatibility Matrix
- Security Analysis & Hardening
- Performance Optimization Benchmark Guidelines
- Manual & Automated Testing Protocols
- Troubleshooting & Comprehensive FAQ
- Contribution Guidelines
- Licensing Details
ChronoTimer Utility is a professional, high-performance timekeeping application designed to run entirely in the browser. It features a high-precision stopwatch with centisecond accuracy, a customizable countdown timer with a visual progress indicator, and a Pomodoro focus assistant. Built using pure, modern vanilla HTML5, CSS3, and JavaScript, it is lightweight, secure, and offline-capable.
- Frontend: Pure HTML5, CSS3 Custom Properties (Variables), Vanilla JavaScript
- Audio Engine: Web Audio API (Native browser synthesizer)
- Storage: LocalStorage API
- Icons: Inline scalable vector graphics (SVGs)
| Feature Group | Capabilities | Technical Implementation |
|---|---|---|
| High-Precision Stopwatch | Centisecond tracking, infinite lap recording, fastest/slowest lap comparison, real-time delta tracking, CSV data export. | requestAnimationFrame render loop, monotonic clock offset delta calculations, Client-side Blob generation. |
| Countdown Timer | Hours/Minutes/Seconds pickers, 4 quick-select presets, circular progress visualizer, automatic alarm triggers. | SVG stroke-dashoffset transition, state caching, Web Audio synthesizer tone sweeps. |
| Pomodoro Focus Timer | Focus/Break states, automatic phase advancement, visual progress cycles, configurable work/break durations. | State machine tracking Pomodoro sessions, LocalStorage user settings synchronization. |
| Synthesized Tone Engine | Non-blocking ticks, click feedback, double-beep alerts, rising/descending scale sweeps. | Web Audio API oscillator nodes, exponential gain decay envelopes. |
| Premium Design System | Dark glassmorphism, responsive mobile layouts, bottom sheet dialogs, zero layouts shift (tabular monospace numbers). | CSS variables, Outfit/JetBrains Mono fonts, hardware-accelerated transforms. |
| Accessibility & Shortcuts | Full keyboard control, ARIA labels, contrast ratios, modal overlays. | Global keypress event capturing, semantic HTML elements. |
This application is designed as a modular, single-page application using vanilla ES6 modules. The DOM interface binds directly to specific state controllers.
graph TD
User([User Interface]) --> DOM[HTML DOM Elements]
DOM --> Controller[App Event Listener / Switchboard]
subgraph Controllers [State Controllers]
Controller --> SW[Stopwatch Controller]
Controller --> TR[Timer Controller]
Controller --> PM[Pomodoro Controller]
end
subgraph Hardware [Hardware Services]
SW --> RAF[requestAnimationFrame Monotonic Loop]
TR --> RAF
PM --> RAF
SW --> Storage[LocalStorage State Cache]
TR --> Storage
PM --> Storage
end
subgraph AudioEngine [Native Audio Engine]
TR --> Synthesizer[Web Audio API AudioContext]
PM --> Synthesizer
Controller --> Synthesizer
Synthesizer --> Osc[Oscillator Nodes]
Osc --> Gain[Gain Nodes/Decay Envelope]
Gain --> Destination[Audio Output Device]
end
The application flows through different tab modes and states. Active timers pause background tasks when switching, ensuring CPU resources are focused on the active panel.
stateDiagram-v2
[*] --> Idle : Application Load
state Idle {
[*] --> StopwatchMode
StopwatchMode --> TimerMode : Switch Tab
TimerMode --> PomodoroMode : Switch Tab
PomodoroMode --> StopwatchMode : Switch Tab
}
state StopwatchMode {
[*] --> SW_Stopped
SW_Stopped --> SW_Running : Click Start / Space
SW_Running --> SW_Running : Click Lap / L (Record Split)
SW_Running --> SW_Paused : Click Pause / Space
SW_Paused --> SW_Running : Click Resume / Space
SW_Paused --> SW_Stopped : Click Reset / R
}
state TimerMode {
[*] --> TR_Setup
TR_Setup --> TR_Running : Select Duration & Start
TR_Running --> TR_Paused : Click Pause / Space
TR_Paused --> TR_Running : Click Resume / Space
TR_Paused --> TR_Setup : Click Reset / R
TR_Running --> TR_Alarm : Duration = 0
TR_Alarm --> TR_Setup : Alarm Ends / Click Reset
}
state PomodoroMode {
[*] --> PM_Work_Setup
PM_Work_Setup --> PM_Work_Running : Click Start Focus
PM_Work_Running --> PM_Work_Paused : Click Pause
PM_Work_Paused --> PM_Work_Running : Click Resume
PM_Work_Running --> PM_Break_Running : Focus Complete (Trigger Alarm)
PM_Break_Running --> PM_Work_Running : Break Complete (Trigger Alarm)
PM_Work_Running --> PM_Work_Setup : Click Reset / Cycle Ends
}
To prevent clock drift from JS thread blocks, the timing logic compares the current timestamp against the initial start time.
sequenceDiagram
autonumber
actor User as User
participant UI as DOM Display
participant JS as Stopwatch Controller
participant Engine as browser Hardware Timer (performance.now)
participant Audio as Web Audio API Synth
User->>JS: Click Start / Press Space
JS->>Audio: trigger click sound beep
Audio-->>User: audible click feedback
JS->>Engine: get start offset (startTime = performance.now)
JS->>JS: start requestAnimationFrame loop
loop Animation Frame Tick
JS->>Engine: get current timestamp
Engine-->>JS: timeDelta = current - startTime
JS->>JS: format ms into hours, mins, secs, centiseconds
JS->>UI: update textContent (tabular mono text)
end
User->>JS: Click Pause / Press Space
JS->>JS: cancelAnimationFrame
JS->>Engine: compute accumulated elapsedTime
JS->>UI: lock display figures
The Pomodoro cycle tracks sessions and automatically transitions through Work, Short Break, and Long Break phases.
flowchart TD
Start([Initialize Pomodoro]) --> WorkSession[Start Focus Session: 25 Mins]
WorkSession --> WorkRunning{Timer Ticking}
WorkRunning -- Tick --> WorkRunning
WorkRunning -- Session Done --> PlayAlarm1[Play Focus Alert Sound]
PlayAlarm1 --> IncCycle[Increment Focus Count]
IncCycle --> CheckCycles{Completed 4 Cycles?}
CheckCycles -- No --> ShortBreak[Start Short Break: 5 Mins]
ShortBreak --> ShortRunning{Timer Ticking}
ShortRunning -- Tick --> ShortRunning
ShortRunning -- Break Done --> PlayAlarm2[Play Break Alert Sound]
PlayAlarm2 --> WorkSession
CheckCycles -- Yes --> LongBreak[Start Long Break: 15 Mins]
LongBreak --> LongRunning{Timer Ticking}
LongRunning -- Tick --> LongRunning
LongRunning -- Break Done --> PlayAlarm3[Play Cycle Complete Sound]
PlayAlarm3 --> ResetCycle[Reset Focus Cycle Count]
ResetCycle --> WorkSession
The repository is structured as a clean static project. It requires no installation wrappers, compilations, or dependency folders:
ChronoTimer-Utility/
βββ .git/ # Git configuration directory
βββ logo.png # Application logo and favicon source
βββ index.html # HTML structure, inline SVGs, and modals
βββ style.css # CSS style variables and animations
βββ main.js # Core JS state controllers and audio engine
To run the project locally, you only need a modern web browser.
You can open the project directly in your browser:
- Clone the repository:
git clone https://github.com/TheOrionGD/ChronoTimer-Utility.git
- Navigate into the folder:
cd ChronoTimer-Utility - Open
index.htmlin your browser:- Windows: Double-click
index.htmlor runstart index.htmlin PowerShell. - macOS: Run
open index.htmlin Terminal. - Linux: Run
xdg-open index.html.
- Windows: Double-click
To prevent CORS complications and allow full local storage features, run the application using a lightweight local web server:
Using NodeJS:
# Install http-server globally
npm install -g http-server
# Run the server on port 8000
http-server -p 8000Using Python 3:
python -m http.server 8000Using PHP:
php -S localhost:8000Once started, open your browser and navigate to:
http://localhost:8000
The structural logic in index.html uses semantic HTML5 tags and inline vector assets. This provides structured markup, accessible elements, and independent graphics rendering.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="ChronoTimer - A premium, high-precision timing utility featuring a stopwatch, countdown timer, and Pomodoro focus helper with synthesized audio cues.">
<title>ChronoTimer Utility</title>
<link rel="icon" type="image/png" href="logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>Contains application identification controls, shortcut list buttons, and audio toggle interfaces.
<header class="app-header">
<div class="logo">
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="M20 12h2"></path>
<path d="M2 12h2"></path>
</svg>
<span class="logo-text">ChronoTimer <span class="logo-accent">Utility</span></span>
</div>
<div class="header-actions">
<button id="shortcutsBtn" class="icon-btn" title="Keyboard Shortcuts" aria-label="Keyboard Shortcuts">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
<line x1="6" y1="8" x2="6.01" y2="8"></line>
<line x1="10" y1="8" x2="10.01" y2="8"></line>
<line x1="14" y1="8" x2="14.01" y2="8"></line>
<line x1="18" y1="8" x2="18.01" y2="8"></line>
<line x1="6" y1="12" x2="6.01" y2="12"></line>
<line x1="10" y1="12" x2="14.01" y2="12"></line>
<line x1="18" y1="12" x2="18.01" y2="12"></line>
<line x1="6" y1="16" x2="6.01" y2="16"></line>
<line x1="10" y1="16" x2="10.01" y2="16"></line>
<line x1="14" y1="16" x2="14.01" y2="16"></line>
<line x1="18" y1="16" x2="18.01" y2="16"></line>
</svg>
</button>
<button id="soundToggleBtn" class="icon-btn" title="Toggle Sound" aria-label="Toggle Sound">
<svg class="sound-on-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>
<svg class="sound-off-icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<line x1="23" y1="9" x2="17" y2="15"></line>
<line x1="17" y1="9" x2="23" y2="15"></line>
</svg>
</button>
</div>
</header>Uses ARIA states to manage tab selection and panel visibility:
<nav class="tab-bar">
<button class="tab-btn active" data-tab="stopwatch" id="tab-stopwatch">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tab-icon">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
Stopwatch
</button>
<button class="tab-btn" data-tab="timer" id="tab-timer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tab-icon">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
Timer
</button>
<button class="tab-btn" data-tab="pomodoro" id="tab-pomodoro">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tab-icon">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="M12 6v6l4 2"></path>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
Pomodoro
</button>
</nav>The visual framework in style.css handles glassmorphic styling, animations, layouts, and responsive constraints.
Defines color themes, fonts, overlays, buttons, animations, and transitions.
:root {
--font-ui: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-timer: 'JetBrains Mono', monospace;
/* Colors */
--bg-main: #09090e;
--panel-bg: rgba(18, 18, 26, 0.65);
--panel-border: rgba(255, 255, 255, 0.08);
--panel-border-focus: rgba(0, 212, 255, 0.3);
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--text-dark: #6b7280;
--accent-cyan: #00d4ff;
--accent-cyan-glow: rgba(0, 212, 255, 0.4);
--accent-teal: #10b981;
--accent-teal-glow: rgba(16, 185, 129, 0.4);
--accent-tomato: #ff4757;
--accent-tomato-glow: rgba(255, 71, 87, 0.4);
--accent-orange: #f97316;
--accent-orange-glow: rgba(249, 115, 22, 0.4);
--btn-primary-bg: rgba(0, 212, 255, 0.15);
--btn-primary-border: rgba(0, 212, 255, 0.3);
--btn-primary-text: #00d4ff;
--btn-secondary-bg: rgba(255, 255, 255, 0.05);
--btn-secondary-border: rgba(255, 255, 255, 0.1);
--btn-secondary-text: #f3f4f6;
--btn-danger-bg: rgba(255, 71, 87, 0.1);
--btn-danger-border: rgba(255, 71, 87, 0.25);
--btn-danger-text: #ff4757;
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}Establishes the centering flex container and viewport constraints:
body {
font-family: var(--font-ui);
background-color: var(--bg-main);
color: var(--text-main);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden; /* Lock viewport scrolling on desktop */
position: relative;
padding: 20px;
}
.app-container {
width: 100%;
max-width: 480px;
background: var(--panel-bg);
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
border: 1px solid var(--panel-border);
border-radius: 24px;
padding: 30px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 25px;
z-index: 10;
}Floating light elements create ambient background effects. We use will-change: transform and translate3d to enable GPU acceleration, avoiding CPU rendering overhead.
.bg-glow {
position: absolute;
border-radius: 50%;
filter: blur(100px);
z-index: -1;
opacity: 0.35;
pointer-events: none;
will-change: transform;
transform: translate3d(0, 0, 0);
}
.bg-glow-1 {
width: 400px;
height: 400px;
background: radial-gradient(circle, var(--accent-cyan) 0%, transparent 70%);
top: -10%;
left: -10%;
animation: floatGlow 15s infinite alternate ease-in-out;
}
.bg-glow-2 {
width: 500px;
height: 500px;
background: radial-gradient(circle, var(--accent-orange) 0%, transparent 70%);
bottom: -15%;
right: -10%;
animation: floatGlow2 20s infinite alternate ease-in-out;
}
@keyframes floatGlow {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(80px, 50px) scale(1.1); }
}
@keyframes floatGlow2 {
0% { transform: translate(0, 0) scale(1.1); }
100% { transform: translate(-100px, -60px) scale(0.9); }
}Adapts the layout based on viewport size. On screens wider than 480px, modals are centered cards. On smaller viewports, they slide up from the bottom as native drawers.
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 1;
transition: opacity var(--transition-normal);
}
.modal-card {
width: 90%;
max-width: 400px;
background: rgba(20, 20, 30, 0.9);
border: 1px solid var(--panel-border);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
animation: modalSlideIn var(--transition-normal) forwards;
}
@media (max-width: 480px) {
body {
padding: 8px;
align-items: flex-start;
padding-top: 20px;
padding-bottom: 20px;
overflow-y: auto; /* Allow scrolling on short mobile viewports */
}
.modal-backdrop {
align-items: flex-end; /* Align modal to bottom */
}
.modal-card {
width: 100%;
max-width: 100%;
border-radius: 24px 24px 0 0;
margin: 0;
box-shadow: 0 -10px 35px rgba(0, 0, 0, 0.4);
animation: bottomSheetSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes bottomSheetSlideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
}The application logic in main.js is split into independent sub-modules that interact with the shared global settings state.
Manages centisecond precision loops. It tracks lap splits and dynamically compares durations to highlight performance deltas.
const Stopwatch = {
running: false,
elapsedTime: 0,
startTime: 0,
animationFrameId: null,
laps: [],
init() {
DOM.swStartPauseBtn.addEventListener('click', () => this.toggle());
DOM.swLapBtn.addEventListener('click', () => this.recordLap());
DOM.swResetBtn.addEventListener('click', () => this.reset());
DOM.exportLapsBtn.addEventListener('click', () => this.exportCSV());
this.updateDisplayUI();
},
toggle() {
audioAlerts.click();
if (this.running) {
this.pause();
} else {
this.start();
}
},
start() {
this.running = true;
this.startTime = performance.now() - this.elapsedTime;
DOM.swStartPauseBtn.querySelector('.btn-text').textContent = 'Pause';
DOM.swStartPauseBtn.className = 'btn btn-secondary';
DOM.swLapBtn.disabled = false;
DOM.swResetBtn.disabled = false;
const tick = () => {
if (!this.running) return;
this.elapsedTime = performance.now() - this.startTime;
this.updateDisplayUI();
this.animationFrameId = requestAnimationFrame(tick);
};
this.animationFrameId = requestAnimationFrame(tick);
},
pause() {
this.running = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
DOM.swStartPauseBtn.querySelector('.btn-text').textContent = 'Resume';
DOM.swStartPauseBtn.className = 'btn btn-primary btn-start';
},
reset() {
audioAlerts.back();
this.running = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
this.elapsedTime = 0;
this.laps = [];
DOM.swStartPauseBtn.querySelector('.btn-text').textContent = 'Start';
DOM.swStartPauseBtn.className = 'btn btn-primary btn-start';
DOM.swLapBtn.disabled = true;
DOM.swResetBtn.disabled = true;
DOM.exportLapsBtn.classList.add('hidden');
this.updateDisplayUI();
this.renderLapsTable();
},
recordLap() {
audioAlerts.lap();
const splitTime = this.elapsedTime;
const previousLapsSum = this.laps.reduce((acc, l) => acc + l.lapDurationMs, 0);
const lapDuration = splitTime - previousLapsSum;
const lapNumber = this.laps.length + 1;
this.laps.push({
lapNumber,
splitTimeMs: splitTime,
lapDurationMs: lapDuration
});
if (this.laps.length > 0) {
DOM.exportLapsBtn.classList.remove('hidden');
}
this.renderLapsTable();
},
renderLapsTable() {
if (this.laps.length === 0) {
DOM.lapsTableBody.innerHTML = `
<tr class="no-laps-row">
<td colspan="4">No laps recorded yet. Click 'Lap' when running.</td>
</tr>
`;
return;
}
let minIdx = -1;
let maxIdx = -1;
if (this.laps.length >= 2) {
let minVal = Infinity;
let maxVal = -Infinity;
this.laps.forEach((lap, idx) => {
if (lap.lapDurationMs < minVal) {
minVal = lap.lapDurationMs;
minIdx = idx;
}
if (lap.lapDurationMs > maxVal) {
maxVal = lap.lapDurationMs;
maxIdx = idx;
}
});
}
const rows = [...this.laps].reverse().map((lap, index) => {
const originalIndex = this.laps.length - 1 - index;
let rowClass = '';
let relativeLabel = '';
if (originalIndex === minIdx) {
rowClass = 'lap-row-fastest';
relativeLabel = 'Fastest';
} else if (originalIndex === maxIdx) {
rowClass = 'lap-row-slowest';
relativeLabel = 'Slowest';
} else if (originalIndex > 0) {
const diff = lap.lapDurationMs - this.laps[originalIndex - 1].lapDurationMs;
const diffT = formatMs(Math.abs(diff));
const sign = diff >= 0 ? '+' : '-';
relativeLabel = `${sign}${diffT.s}.${diffT.ms}s`;
} else {
relativeLabel = '-';
}
return `
<tr class="${rowClass}">
<td>#${lap.lapNumber}</td>
<td>${this.formatLapTime(lap.lapDurationMs)}</td>
<td>${this.formatLapTime(lap.splitTimeMs)}</td>
<td>${relativeLabel}</td>
</tr>
`;
}).join('');
DOM.lapsTableBody.innerHTML = rows;
}
};Manages the visual countdown loop and updates the stroke outline of the SVG progress ring.
const Timer = {
running: false,
duration: 600000,
remainingTime: 600000,
startTime: 0,
animationFrameId: null,
init() {
DOM.timerStartPauseBtn.addEventListener('click', () => this.toggle());
DOM.timerResetBtn.addEventListener('click', () => this.reset());
[DOM.timerHours, DOM.timerMinutes, DOM.timerSeconds].forEach(input => {
input.addEventListener('change', () => this.updateDurationFromPickers());
});
DOM.presetBtns.forEach(btn => {
btn.addEventListener('click', () => {
const seconds = parseInt(btn.getAttribute('data-seconds'));
this.setDuration(seconds * 1000);
});
});
},
updateProgressRing(ratio) {
const radius = 100;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (ratio * circumference);
DOM.timerProgressCircle.style.strokeDashoffset = offset;
}
};Manages work and break state machine loops, automatically transitioning between cycles.
const Pomodoro = {
running: false,
state: 'work',
cycle: 1,
remainingTime: 0,
duration: 0,
startTime: 0,
animationFrameId: null,
nextState() {
this.running = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
if (this.state === 'work') {
audioAlerts.alertPomo(false); // Play break alert sound
if (this.cycle < 4) {
this.applyState('short', true);
} else {
this.applyState('long', true);
}
} else {
audioAlerts.alertPomo(true); // Play work alert sound
if (this.state === 'long') {
this.cycle = 1;
} else {
this.cycle++;
}
this.applyState('work', true);
}
}
};The sound system synthesizes audio alerts in the browser using the Web Audio API.
OscillatorNode (Freq) βββ> GainNode (Envelope decay) βββ> Destination (Output)
function playTone(frequency, duration, type = 'sine', volume = 0.08) {
if (isMuted) return;
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(frequency, ctx.currentTime);
// Apply exponential decay to prevent audio pops
gainNode.gain.setValueAtTime(volume, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.00001, ctx.currentTime + duration);
osc.connect(gainNode);
gainNode.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + duration);
} catch (err) {
console.warn('Audio synthesis failed:', err);
}
}| Indicator Preset | Trigger Conditions | Tone Parameters | Web Audio Representation |
|---|---|---|---|
Click |
User clicks control buttons. |
|
playTone(800, 0.06, 'sine', 0.05) |
Back/Reset |
Reset button actions. |
|
playTone(500, 0.06, 'sine', 0.05) |
Lap Click |
Lap button click. |
|
playTone(1200, 0.12, 'triangle', 0.08) |
Second Tick |
Every 1-second countdown interval. |
|
playTone(1600, 0.015, 'sine', 0.02) |
Timer Alarm |
Countdown timer reaches 0. | Synthesized square wave pattern, 4 double-beeps at |
Alternating frequencies mapped on active Audio Intervals |
Focus Shift |
Pomodoro Work state ends. | Descending frequency sweep ( |
Programmatic frequency modulation sweep |
Break Shift |
Pomodoro Break state ends. | Rising frequency sweep ( |
Programmatic frequency modulation sweep |
Keyboard shortcuts allow you to control the application without using a mouse. They are captured by a global event listener.
function initKeyboardShortcuts() {
window.addEventListener('keydown', (e) => {
// Avoid triggering shortcuts when typing in numeric inputs
if (e.target.tagName === 'INPUT') return;
const key = e.key.toLowerCase();
if (key === ' ') {
e.preventDefault(); // Prevent page scrolling
if (currentTab === 'stopwatch') {
Stopwatch.toggle();
} else if (currentTab === 'timer') {
Timer.toggle();
} else if (currentTab === 'pomodoro') {
Pomodoro.toggle();
}
} else if (key === 'l') {
if (currentTab === 'stopwatch') {
e.preventDefault();
if (Stopwatch.running) {
Stopwatch.recordLap();
}
}
} else if (key === 'r') {
e.preventDefault();
if (currentTab === 'stopwatch') {
Stopwatch.reset();
} else if (currentTab === 'timer') {
Timer.reset();
} else if (currentTab === 'pomodoro') {
Pomodoro.reset();
}
} else if (key === 't') {
e.preventDefault();
// Cycle through tabs: Stopwatch -> Timer -> Pomodoro
const currentIndex = CONFIG.modes.indexOf(currentTab);
const nextIndex = (currentIndex + 1) % CONFIG.modes.length;
switchTab(CONFIG.modes[nextIndex]);
}
});
}The application can be configured by modifying variables directly in the codebase:
let settings = {
isMuted: false,
timerDuration: 600000, // 10 minutes default (in ms)
pomoDurations: {
work: 25, // Focus duration in minutes
short: 5, // Short break duration in minutes
long: 15 // Long break duration in minutes
}
};The circular countdown rings use a stroke offset to visualize progress:
.progress-ring-fill {
stroke-dasharray: 628.3;
stroke-dashoffset: 628.3; /* Adjusted dynamically in JS */
}The application uses modern, native browser APIs. Below is the minimum version compatibility matrix:
| Feature API | Chrome | Firefox | Edge | Safari | Opera | iOS Safari | Android Chrome |
|---|---|---|---|---|---|---|---|
requestAnimationFrame |
20+ | 23+ | 12+ | 6.1+ | 15+ | 7.1+ | 20+ |
performance.now() |
24+ | 15+ | 12+ | 8+ | 15+ | 9+ | 24+ |
AudioContext (Web Audio) |
35+ | 25+ | 12+ | 14.1+ | 22+ | 14.5+ | 35+ |
backdrop-filter (CSS) |
76+ | 103+ | 79+ | 9+ | 63+ | 9+ | 76+ |
localStorage |
4+ | 3.5+ | 12+ | 4+ | 10.5+ | 3.2+ | 4+ |
The application is built to run entirely on the client side, eliminating common server-side security vulnerabilities.
To prevent Cross-Site Scripting (XSS) attacks, configure your server to serve the application with a strict CSP header:
Content-Security-Policy: default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; media-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none';To prevent data corruption or prototype pollution from modified LocalStorage data, the app sanitizes loaded configuration objects:
function loadSettings() {
try {
const saved = localStorage.getItem(CONFIG.storageKey);
if (saved) {
const parsed = JSON.parse(saved);
// Ensure values are numbers, preventing script injection
if (typeof parsed.isMuted === 'boolean') settings.isMuted = parsed.isMuted;
if (Number.isInteger(parsed.timerDuration)) settings.timerDuration = parsed.timerDuration;
if (parsed.pomoDurations) {
if (Number.isInteger(parsed.pomoDurations.work)) settings.pomoDurations.work = parsed.pomoDurations.work;
if (Number.isInteger(parsed.pomoDurations.short)) settings.pomoDurations.short = parsed.pomoDurations.short;
if (Number.isInteger(parsed.pomoDurations.long)) settings.pomoDurations.long = parsed.pomoDurations.long;
}
}
} catch (err) {
console.warn('Failed to sanitize cache configuration:', err);
}
}The codebase is optimized for performance to ensure smooth animations and low CPU usage.
- No Layout Thrashing: Read operations are separated from style write operations during animation frame renders.
- Layer Separation: The background moving glows are placed on their own GPU layers using
will-change: transform. This avoids re-rendering the entire layout during background animations. - Frame Coalescing: Timer updates are synchronized with the browser's refresh rate using
requestAnimationFrame. This prevents unnecessary calculations when the page is minimized or backgrounded.
- Active Garbage Collection: Oscillator nodes and filters created during audio synthesis are immediately released after playback. This prevents memory leaks from unused audio nodes.
- Deferred Loops: Inactive panels pause their active logic loops, reducing background CPU usage.
To verify timer accuracy under simulated load:
- Open the browser's developer console (
F12). - Run the script below to benchmark the timer delta calculations:
console.time("PrecisionTimerDeltaTest");
let testStartTime = performance.now();
let frameCount = 0;
function runTest() {
frameCount++;
let current = performance.now();
let delta = current - testStartTime;
if (frameCount < 100) {
requestAnimationFrame(runTest);
} else {
console.log(`Completed 100 cycles. Elapsed: ${delta}ms. Avg frame rate: ${(frameCount / (delta / 1000)).toFixed(2)} fps.`);
console.timeEnd("PrecisionTimerDeltaTest");
}
}
requestAnimationFrame(runTest);| Target Field ID | Input Value Type | Test Condition Case | Expected Validation Result |
|---|---|---|---|
timerHours |
Text String ("abc") |
Entering alphabetical strings. | Input value sanitizes to 0 automatically. |
timerMinutes |
Numeric Float (25.5) |
Entering floating-point decimals. | Rounded down to 25 minutes automatically. |
timerSeconds |
Out-of-bounds (99) |
Entering seconds values greater than 59. | Value clamped to maximum parameter of 59 seconds. |
pomoWorkDuration |
Negative (-10) |
Entering values below minimum bounds. | Value clamped to minimum parameter of 1 minute. |
pomoShortDuration |
Extreme High (120) |
Entering values above maximum bounds. | Value clamped to maximum parameter of 30 minutes. |
This is typically caused by hardware acceleration conflicts on older browsers. You can resolve this by adding a perspective style filter to the container to force GPU rendering:
.progress-ring-svg {
transform: rotate(-90deg) translateZ(0);
}Audio cracking can occur if multiple oscillators are created without proper node cleanup. Our synthesizer automatically releases nodes when audio finishes, but you can also configure an audio compressor node to prevent clipping:
const compressor = ctx.createDynamicsCompressor();
compressor.threshold.setValueAtTime(-50, ctx.currentTime);
compressor.knee.setValueAtTime(40, ctx.currentTime);
compressor.ratio.setValueAtTime(12, ctx.currentTime);
compressor.attack.setValueAtTime(0, ctx.currentTime);
compressor.release.setValueAtTime(0.25, ctx.currentTime);
// Connect nodes through the compressor
gainNode.connect(compressor);
compressor.connect(ctx.destination);The timer calculations use performance.now(), which measures time relative to the page life cycle. Closing the page resets the timing variables. If you need to preserve timer states across page reloads, configure the app to sync the absolute system time to localStorage:
// Caching absolute timestamps
function saveStateToStore() {
localStorage.setItem('sw_timestamp', Date.now());
}Contributions are welcome! Please follow these steps to contribute:
- Fork the repository.
- Create a new branch:
git checkout -b feature/your-feature-name
- Commit your changes:
git commit -m "Add your commit message" - Push your branch:
git push origin feature/your-feature-name
- Open a Pull Request.
This project is open-source software licensed under the MIT License. See the LICENSE file for details.