There's something oddly satisfying about building a tool that thousands of people might use daily, especially when you decide to do it with zero dependencies. No React, no Vue, no moment.js — just good old-fashioned vanilla JavaScript, HTML, and CSS.
Last month, I needed a countdown timer for an upcoming project deadline. I could have downloaded any of the hundreds of countdown apps available. But as developers, we often find ourselves thinking, "I could build this myself in an afternoon." Spoiler alert: it took longer than an afternoon, but the journey was worth it.
The Problem with Existing Solutions
Most countdown timer tools online are either:
- Bloated with features I didn't need (who needs a countdown with confetti animations?)
- Require desktop installation (I wanted something browser-based)
- Have terrible mobile responsiveness (nobody wants to pinch-zoom on a timer)
- Are riddled with ads and tracking scripts
I wanted something simple: set a date, see the time remaining, get a visual progress indicator. That's it. No account creation, no email signup, no "premium features" paywall.
The Architecture Decision: Why Vanilla?
When I started planning, I caught myself reaching for React out of habit. But then I thought about it — this is a single-purpose tool with minimal state. Do I really need a virtual DOM for a timer?
The answer was no. Here's my reasoning:
// The entire state management for this app
let targetDate = null;
let eventName = '';
let startDate = null;
That's it. Two variables and a timer interval. Adding React would have meant:
- A build process
- Bundle size overhead
- More complexity than the actual problem requires
Sometimes the simplest solution is the right one. This was one of those times.
The Core Challenge: Time Calculation
The heart of any countdown timer is the time calculation logic. It sounds simple — subtract one date from another — but there are edge cases that'll bite you if you're not careful.
function updateCountdown() {
const now = new Date();
const diff = targetDate - now;
if (diff <= 0) {
// Handle expired state
showExpired(Math.abs(diff));
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
// Update DOM
}
The tricky part? Timezone handling. If someone in Tokyo sets a target date, and someone in New York views it, the calculations need to be consistent. I initially used new Date() everywhere, but quickly realized I needed to be more careful with how I stored and compared dates.
The "Works on My Machine" Moment
Here's where things got interesting. I tested the timer locally, and everything worked perfectly. Then I deployed it, and a user in Australia reported that the countdown was off by a day.
Classic "works on my machine" situation.
The issue? I was using local timezone offsets implicitly. When a user selected a date in their local timezone, I needed to store it as a timestamp (UTC) to ensure consistent calculations across timezones.
// The fix: convert to UTC timestamp when storing
const targetTimestamp = new Date(dateInput.value).getTime();
This was a humbling reminder that even "simple" time calculations have hidden complexity.
AI-Assisted Development: The Honest Truth
Now, here's where I need to be completely transparent. I used AI tools (mostly Claude and ChatGPT) throughout this project. Here's how it actually went — the good, the bad, and the ugly.
What AI Did Well
The initial skeleton: I described my requirements — a single-file HTML tool with dark mode support, i18n, and responsive design — and the AI generated a solid starting point in minutes. The CSS variables structure, the basic layout, and the initial JavaScript logic were all well-structured.
The i18n implementation: I asked for a lightweight internationalization system that supported English and Chinese, with language detection based on URL parameters and browser settings. The AI came up with a clean solution:
const i18n = {
zh: { title: '倒计时器', days: '天', hours: '时' },
en: { title: 'Countdown Timer', days: 'days', hours: 'hrs' }
};
Dark mode: The media query for prefers-color-scheme was generated perfectly. No complaints there.
Where AI Struggled
The timezone bug: I spent two hours debugging a timezone issue that AI kept getting wrong. Every time I described the problem, it would suggest a fix that worked for the specific case but broke another edge case. Eventually, I had to reason through it myself and implement the solution manually.
Progress bar logic: The AI's initial implementation of the progress bar was mathematically incorrect for the "expired" state. It took several iterations of me explaining the requirements before it got the logic right.
Performance optimization: The AI initially used setInterval with DOM updates every 100ms, which caused unnecessary reflows. I had to point out that we only need to update once per second, and that we should batch DOM updates.
The Iterative Process
Working with AI felt like pair programming with a very enthusiastic junior developer. It's fast, eager, and sometimes confidently wrong. Here's what my workflow looked like:
- Describe the feature: "I need a preset button that sets the target date to next New Year's Day"
- Get the initial implementation: AI generates code that mostly works
- Test and break it: Find the edge case that fails
- Report the bug: Describe what happened and what should have happened
- Iterate: Repeat until it works correctly
For example, the "New Year" preset initially set the date to January 1st of the current year, which meant it was always in the past. The fix required understanding the business logic:
function setNewYearPreset() {
const now = new Date();
const nextYear = now.getFullYear() + 1;
targetDate = new Date(nextYear, 0, 1); // January 1, next year
}
The "Aha" Moment: Single File Architecture
One of my best decisions was keeping everything in a single HTML file. Here's why:
- No build step: Edit, save, refresh. That's the entire development loop.
- Easy deployment: Just upload one file to any static host.
- Instant sharing: Users can save the file locally and use it offline.
- Performance: No network requests for assets — everything is inline.
The trade-off? The file gets large (around 15KB for this project), but for a utility tool, that's perfectly acceptable. Users get instant load times with zero requests.
The Progress Bar Problem
The progress bar was trickier than expected. If the user sets a start date, we can show progress. But what if they don't? I needed to handle three states:
- No start date set → Show no progress bar
- Start date in the past → Calculate progress as
(now - start) / (target - start) - Target date passed → Show 100% (or switch to "expired" visual)
The AI kept overcomplicating this. It wanted to create a full state machine. I simplified it to:
function updateProgress() {
if (!startDate) return;
const total = targetDate - startDate;
const elapsed = now - startDate;
const percent = Math.min(100, Math.max(0, (elapsed / total) * 100));
// Update progress bar width
}
Sometimes the simplest solution is the right one, even if it doesn't handle every edge case perfectly.
Lessons Learned
1. AI is a multiplier, not a replacement: It made me 3x faster at writing boilerplate code, but I still needed to understand the domain deeply to catch its mistakes.
2. Time is always more complex than you think: Timezones, daylight saving, leap years — these aren't edge cases, they're the norm. Always test with real-world dates.
3. Single-file apps have a place: For utility tools, the simplicity of deployment and usage outweighs the benefits of a build system.
4. Performance matters, even for simple tools: The difference between updating the DOM every 100ms vs. every 1000ms might not seem like much, but it affects CPU usage and battery life on mobile devices.
5. Dark mode isn't optional anymore: Users expect it. The prefers-color-scheme media query is your friend.
The Result
After about three days of work (two of which were debugging timezone issues), I had a working countdown timer that:
- Works entirely in the browser
- Supports English and Chinese
- Has dark mode
- Is fully responsive down to 320px
- Updates in real-time
- Handles expired states gracefully
- Weighs less than 20KB total
During this process, I built a small browser-based tool to make this workflow easier. You can try it here: Craftvo Countdown Timer
Final Thoughts
Building a countdown timer taught me that "simple" tools often have hidden complexity. It also showed me that the right tool for the job might be simpler than you think — sometimes a single HTML file is all you need.
The AI-assisted development experience was genuinely positive, but it's not magic. It's like having a fast, enthusiastic pair programmer who sometimes needs guidance. The key is knowing enough to catch its mistakes and guide it in the right direction.
And remember: it's always a timezone issue. It's always a timezone issue.
Top comments (0)