Timestripe

Timestripe is a goal-centric productivity app. I joined three founders in late 2019 as the first engineer on the team. I took full responsibility for the frontend development and started building the product from scratch. During my time there I built many fascinating things. Here I'd like to share some of them.

Swiper component

Swiper is my favorite UI component that I ever built.

Basically it is a controlled virtualized horizontally scrollable carousel component that supports nested sortable items and scrolling with touch gestures.

Let's unpack it step by step. First, it is very important for this component to be virtualized: we want user to be able to scroll to any day in the past or the future, so we can't have fixed number of slides (like most of the ready-made carousels do).

It also has to be controlled. It means that current slide should be passed as a prop and updated by the parent component. This allows us to orchestrate multiple Swipers from a single source of truth and implement Timestripe's famous Horizons view.

Button

Every app needs a button. So this is the very first thing I've built for Timestripe.

The interesting thing about this button is a trick I came up with and using it ever since in many other projects: async click handler.

If the onClick handler returns a promise, the button disables itself and shows the spinner until that promise settles. This removes the usual loading state boilerplate and still renders the loader even when you forget to pass the loading prop.

Click the button to see the loading spinner

Making the web app feel native

Timestripe’s “mobile app” in the App Store and Google Play is the same web client running inside a thin native shell. You can install it from the App Store  or Google Play .

I’m not an iOS or Android engineer, and I’m usually the first to complain when companies ship a flimsy web shell instead of a real native app. Timestripe is a self-funded indie team though, so wrapping the web app was the only way we could stay focused on the product without blowing up the budget.

I documented the tricks that make the UI feel native in this post .

Goal storage

Reactive state management is hard. There are (and were) tons of different ways to approach it, and the hardest thing to get right is performance.

At first, Timestripe had a pretty simple React Context based state management which was working very well. The problems started to arise when we started adding offline data storage.

Dropdowns

Dropdown is a very common UI component. Just like with everything else in software engineering, there are no off-the-shelf solutions that would just work in a real product.

So I built my own dropdown component. It supports nested dropdowns (with "inline" mode, more on that later), keyboard navigation, and async click handlers.

The key idea behind inline mode is that nested menus don't pop out to the side (which is awkward on mobile). Instead, they replace the current menu content with a smooth transition, similar to how iOS Settings navigation works. The previous menu becomes hidden but stays in the DOM, and a back button lets you return.

Async click handlers work exactly like in the Button component: if onClick returns a Promise, the menu item shows a spinner and the menu stays open until the operation completes.

Under the hood, the real Timestripe implementation uses Floating UI  for positioning, with a singleton pattern ensuring only one root dropdown is open at a time.

Modals

The cool thing about Timestripe's modals is imperative API. In React the go-to way to handle modals is to use a state variable with conditional rendering. I have found that I often wanted to treat modals as Promises: show a modal, wait for the user to interact with it, and then get a result. With traditional declarative approach this quickly turned into a mess of nested callbacks and state updates.

Here's how the typical declarative approach looks:

And here's the imperative approach I built for Timestripe:

The trick is simple: openModal wraps the modal lifecycle in a Promise. When the component calls close(result), the Promise resolves with that result. This makes sequential flows (like multi-step wizards) read like normal async code.

Try "Chained flow" to see how sequential modals work with async/await

Time blocking view

Time blocking is one of Timestripe's signature features. It lets users drag goals onto a 24-hour calendar grid and visually plan their day.

The most interesting technical challenge here was building the drag-and-drop system from scratch. Libraries like react-beautiful-dnd are great for lists, but a time grid needs pixel-precise positioning with snapping to time intervals. Every mouse/touch movement gets converted from pixels to milliseconds, snapped to a 10-minute grid, and clamped to day boundaries.

Another key piece is the layering algorithm for overlapping blocks. When two goals overlap in time, they need to be rendered side by side rather than on top of each other. The algorithm groups overlapping goals into clusters, then assigns each goal a parallelIndex and calculates the width as 100% / goalsInCluster.

Try dragging the blocks below. "Deep work" and "Code review" overlap intentionally to show the layering. You can also resize blocks by dragging the bottom handle.

Drag to move, bottom handle to resize. Blocks snap to a 10-minute grid.

On mobile, the real implementation also uses long-press detection (300ms threshold) to distinguish between scrolling and dragging, plus iOS haptic feedback when the block snaps to a new position.