Building a custom elements Pomodoro timer: from React to Web Components

For some time now, I've been using the Pomodoro Technique to structure myself and maintain focus when working on complex tasks, such as programming or writing projects. Since 2014, I have been using my own spin on a Pomodoro timer written in React. Recently, I took some time to rewrite it as a custom element. In this post, I would like to share my learnings.

What is the Pomodoro Technique? To quote Wikipedia:

The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It uses a kitchen timer to break work into intervals, typically 25 minutes in length, separated by short breaks.

Personally, I use the sequence of three 25 min work intervals interrupted by 5 min breaks followed by a 25 min work interval interrupted by a longer 15 min break.

Why Custom Elements? While my React-based implementation served its purpose well, I am increasingly using custom elements in my projects. They offer native browser support without dependencies and can be easily integrated in a wide range of contexts. By designing a declarative interface, it possible to make web components compatible with server-side rendering and libraries like htmx.

How to use the timer? To include the tomato timer in a page, simply use the custom element tag <tt-timer>. Once started, the timer will reflect the current state in the history attribute as a sequence of interval with their start times. For example

<tt-timer
    history='["work","2025-03-29T18:05:26.298Z"],["short","2025-03-29T18:30:26.298Z"]'
></tt-timer>

This history attribute is the single source of truth for the timer's state. The timer can be fully controlled by modifying this attribute. For example

  • To start a new interval, add it to the history
  • To stop the timer, remove the history attribute
  • To jump to a specific part of the sequence, modify the history accordingly

Therefore it's also possible to initialize the timer to a given state at rendering time or to update the timer by modifying the element.

The initial state design. When transitioning from React to custom elements, the state design proved challenging. The React version had all state centralized in the outermost component in an object with the keys

{
    nextState: 'work',
    position: null,
    timeRemaining: null,
    state: 'stopped',
}

My first instinct was to map each of these state properties to separate attributes in the custom element. However, this approach revealed several problems:

  • State interdependence. The position in the Pomodoro sequence determines both current and next states, but exposing them as separate attributes created potential inconsistencies
  • Callback timing issues. Such inconsistent states are observed in practice as the attributeChangedCallback is called independently for each attribute
  • Rapidly changing values. The timeRemaining property changes every second, making it impractical to reflect in an attribute that might be controlled externally
  • Infinite recursion risk. Using the attributeChangedCallback to update the DOM, while also updating element attributes created a loop

Improved approach. To address these issues, I completely redesigned the state management.

First, I used a clear flow of updates to remove the infinite recursion:

  • External events, such as user clicks or direct modification, update the attributes
  • These updates trigger the attributeChangedCallback
  • The attributeChangedCallback modifies the DOM

Next, I use the history of user interactions as the state, encoded as intervals with their start times. This history serves as the single source of truth that fully determines the current state. This change eliminated the need for multiple interconnected state properties and ensured state consistency even when modified externally. The state encoded in this way is also slowly changing, over the span of multiple hours it is only updated a couple of times.

However, it also required more complex computation to derive the relevant information necessary to render the user interface. In particular, determining the next state in the sequence requires to match the configured intervals sequence with observed intervals.

Conclusion. Rewriting my Pomodoro timer as a custom element was a fun challenge. In particular, the state management required careful thought. The result is a lightweight component that can be easily integrated into any web project. If you're interested in the end result, you can try the timer here or explore the source code.