Timepicker-UI v4.0.0 – Five Years of Learning, One Major Rewrite
I didn’t rewrite the library because it was broken – I rewrote it because I outgrew it.
Five years. That’s how long I’ve been maintaining timepicker-ui – a framework-agnostic time picker library that started as my “learn TypeScript properly” project.
Last week, I shipped v4.0.0, a complete architectural rewrite that breaks almost everything. And honestly? It feels great.
The Beginning (2019)
I didn’t want to “play with TypeScript” – I wanted to learn it for real.
And the fastest way to do that is to build something painful enough to expose all your mistakes.
A time picker was perfect:
Native <input type="time"> was inconsistent across browsers, Material Design’s version was locked to Google’s ecosystem, and the component was small enough to finish but complex enough to teach real lessons.
TypeScript caught bugs I would’ve spent hours debugging in production. That was the moment I got hooked.
The Quiet Period (2020–2024)
For three years, I barely touched the project. It “worked,” users were quiet, issues were small.
But every time I opened the codebase, I could feel the 2019 architecture. Tight coupling. Hidden any. DOM-first design. No clear boundaries.
It wasn’t broken – it was just from another era of my skill level.
The False Start: v3.0.0 (July 2025)
In July 2025, I shipped v3.0.0 – and at the time, I genuinely thought it was the “big improvement” release.
It introduced:
- EventEmitter API (goodbye DOM events!)
- Material Design 3 themes
- Better performance with RAF batching
“Finally,” I thought. “This fixes everything.”
But after using it for a few weeks, reality landed hard:
- The bundle was still ~80 KB
- Strict mode exposed
anytypes hiding in too many places - The inheritance-based architecture was still the same – just rearranged
I hadn’t redesigned anything.
I had simply moved the mess around.
v3 wasn’t a failure – it was a wake-up call.
A polished band-aid on a foundation I no longer believed in.
The Real Rewrite: v4.0.0 (November 2025)
After working with v3 for three weeks, I finally accepted the truth:
“You’re going to fight this architecture forever unless you burn it down.”
Could I have lived with v3? Yes.
Should I? Absolutely not.
So I rewrote the entire library from scratch:
- Composition-only (zero inheritance)
- Zero
anytypes – strict TypeScript everywhere - SSR-safe by design (no
windowduring import) - 18% smaller bundle (66 KB → and still shrinking)
- Logical, grouped options structure
Does v4 have bugs? Of course.
But the architecture is finally something I’m proud of.
v3 was “good enough.”
v4.0.0 is “finally heading in the right direction.”
Why Rewrite After v3?
Three reasons pushed me over the edge:
1. Composition Over Inheritance
v3’s inheritance hierarchy looked clean… until you needed to extend anything.
class TimepickerBase
// 300 lines of shared logic
class Timepicker extends TimepickerBase
// More inheritance
New architecture:
class TimepickerUI
constructor(
private core: CoreState,
private emitter: EventEmitter,
private managers: Managers
)
No shared parent.
No hidden state.
No magical overrides.
Just composition – explicit, boring, predictable.
2. Strict Typing or Bust
If the type is hard to express, your architecture is probably wrong.
Old code:
function handleEvent(data: any)
const value = data.value;
New code:
interface ConfirmEvent
hour: string;
minutes: string;
type?: "AM"
function handleEvent(data: ConfirmEvent)
const value = data.hour;
Fixing type errors revealed real bugs I didn’t even know I had.
TypeScript didn’t just catch mistakes –
it exposed every architectural decision I had been avoiding.
3. Breaking Changes Are a Feature
For years, I avoided breaking changes because “users will get angry.”
Wrong.
Users don’t fear breaking changes –
they fear unclear breaking changes.
The moment I allowed myself to break APIs, everything became easier.
The library became simpler, cleaner, and more future-proof.
SSR became predictable.
The bundle shrank by deleting features, not adding them.
And testing stopped being a nightmare because logic no longer depended on the DOM.
What Changed
CSS
.timepicker-ui-wrapper → .tp-ui-wrapper;
Options (flat → grouped)
// Before
new TimepickerUI(input, clockType: "12h", theme: "dark" );
// After
new TimepickerUI(input,
clock: type: "12h" ,
ui: theme: "dark" ,
);
Events (DOM → type-safe emitter)
// Before
input.addEventListener("timepicker:confirm", handler);
// After
picker.on("confirm", handler);
Removed
- ❌ Programmatic theming API
- ❌ Legacy DOM events
- ❌ Inheritance-based architecture
Sometimes the best feature is removing a feature.
The Architecture & Integration
CoreState (state)
→ EventEmitter (events)
→ Managers (behavior)
Each piece has one responsibility and no knowledge of the others beyond what’s passed in.
This makes the library predictable, testable, and framework-agnostic.
React Example (Vanilla)
const picker = new TimepickerUI(inputRef.current,
clock: type: "24h" ,
callbacks: onConfirm: (data) => console.log(data) ,
);
picker.create(); // mount
picker.destroy(); // cleanup (avoids stale listeners)
Vue, Svelte, Angular – same rules:
create on mount, destroy on unmount.
Official React Wrapper
After shipping v4, I built an official React wrapper – timepicker-ui-react.
Why? Because wrapping v4 in React properly requires:
- Dynamic imports (SSR safety)
- Proper cleanup (no memory leaks)
- Event bridge (React callbacks → EventEmitter)
- Controlled/uncontrolled patterns
- Zero type duplication (re-export core types)
The wrapper follows the same architectural principles as v4:
- Composition with custom hooks
- SSR-safe by design
- Zero
anytypes - Thin abstraction layer (all logic stays in core)
import Timepicker from "timepicker-ui-react";
function App()
const [time, setTime] = useState("12:00 PM");
return (
<Timepicker
value=time
onUpdate=(data) => setTime(`$data.hour:$data.minutes $data.type`)
options=
clock: type: "12h" ,
ui: theme: "dark" ,
/>
);
It’s not a separate implementation – it’s a proper integration layer that respects both React patterns and timepicker-ui’s architecture.
Try it: https://timepicker-ui.vercel.app/react
Install: npm install timepicker-ui-react
Source: https://github.com/pglejzer/timepicker-ui-react
Stats & Accessibility
-
154,360 npm downloads (2020–2025) -source:
https://npm-stat.com/charts.html?package=timepicker-ui&from=2020-01-21&to=2025-11-21
(This number does not represent real users. Most npm downloads come from CI/CD pipelines, Docker builds, monorepos reinstalling dependencies, and registry mirrors. One company pipeline can generate thousands of installs per month. These stats reflect trend and activity, not the actual size of the user base.) - 66KB minified –18% smaller than v3
-
0 dependencies, 0
anytypes -
SSR-safe by design -no
window/documentaccess during import - 10 built-in themes
- Accessible -ARIA labels, keyboard navigation, proper focus trap
What I Still Don’t Like About v4.0.0
Let’s be honest:
- Test coverage is bad. Architecture is testable now – but I haven’t written enough tests.
- Bundle is still 66 KB. I want it <60 KB.
- No plugin system. Extending the picker currently requires forking – a plugin architecture would make experimentation safer.
- ClockManager is too big. It handles angle math, hand positioning, drag interactions… that should’ve been three managers. Animation logic still leaks into it.
v4 isn’t perfect – it’s just no longer weighed down by old decisions.
The Real Takeaway
A rewrite won’t fix your old mistakes –
but it finally stops you from carrying them into the future.
Future me can build on this foundation.
v3 never gave me that chance.
Install & Use
Try it: https://timepicker-ui.vercel.app/
Install:
npm install timepicker-ui
Use:
import TimepickerUI from "timepicker-ui";
import "timepicker-ui/main.css";
const picker = new TimepickerUI(input,
clock: type: "24h" ,
ui: theme: "dark" ,
callbacks:
onConfirm: (data) =>
console.log("Time:", data);
,
,
);
picker.create();
// Or EventEmitter API
picker.on("confirm", (data) =>
console.log("Time:", data);
);
Full source: https://github.com/pglejzer/timepicker-ui
Star it if you’ve ever fought with browser time pickers.
