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 any types 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 any types – strict TypeScript everywhere
  • SSR-safe by design (no window during 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

Enter fullscreen mode

Exit fullscreen mode

New architecture:

class TimepickerUI 
  constructor(
    private core: CoreState,
    private emitter: EventEmitter,
    private managers: Managers
  ) 

Enter fullscreen mode

Exit fullscreen mode

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;

Enter fullscreen mode

Exit fullscreen mode

New code:

interface ConfirmEvent 
  hour: string;
  minutes: string;
  type?: "AM" 

function handleEvent(data: ConfirmEvent) 
  const value = data.hour;

Enter fullscreen mode

Exit fullscreen mode

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;
Enter fullscreen mode

Exit fullscreen mode




Options (flat → grouped)

// Before
new TimepickerUI(input,  clockType: "12h", theme: "dark" );

// After
new TimepickerUI(input, 
  clock:  type: "12h" ,
  ui:  theme: "dark" ,
);
Enter fullscreen mode

Exit fullscreen mode




Events (DOM → type-safe emitter)

// Before
input.addEventListener("timepicker:confirm", handler);

// After
picker.on("confirm", handler);
Enter fullscreen mode

Exit fullscreen mode




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)
Enter fullscreen mode

Exit fullscreen mode

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)
Enter fullscreen mode

Exit fullscreen mode

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 any types
  • 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" ,
      
    />
  );

Enter fullscreen mode

Exit fullscreen mode

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 minified18% smaller than v3
  • 0 dependencies, 0 any types
  • SSR-safe by design -no window/document access 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
Enter fullscreen mode

Exit fullscreen mode

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);
);
Enter fullscreen mode

Exit fullscreen mode


Full source: https://github.com/pglejzer/timepicker-ui
Star it if you’ve ever fought with browser time pickers.



Source link