DEV Community

Cover image for Why Building a Reliable Calendar UI Is Harder Than It Looks
Neha Mangla
Neha Mangla

Posted on Originally published at nehamangla.com

Why Building a Reliable Calendar UI Is Harder Than It Looks

In a workforce calendar, shifts, absences, and days off can look like variations of the same event. They aren't. A day off belongs to a calendar date; a shift belongs to a span of time. Mixing those rules can move an event to the wrong day without producing an obvious error.

I worked on a workforce calendar using React, TypeScript, and Bryntum. It combined shifts, absences, overtime, and days off from separate services. They looked similar in the calendar, but their date handling and visibility rules were not interchangeable.

A day off that moves to yesterday

Take a day-off record with the value 2026-10-23. Parsing that string as midnight UTC and displaying it in Los Angeles can produce October 22. The browser has converted an instant correctly; the mistake is that a day off was never meant to be an instant.

Input Incorrect interpretation Expected result
2026-10-23 — date-only day off Oct 22 — UTC midnight formatted in Los Angeles Oct 23 — preserve the intended calendar day

A shift timestamp is different: it represents a moment that may be displayed at different local times. We kept those two paths separate. Here's a reduced TypeScript illustration of that boundary:

TypeScript · illustrative example (not production code)

type CalendarRecord =
  | { kind: "dayOff"; date: string }
  | { kind: "shift"; startsAt: string };

function toCalendarEvent(record: CalendarRecord) {
  if (record.kind === "dayOff") {
    // Keep the calendar date as a date, not a UTC timestamp.
    return { kind: "dayOff", calendarDate: record.date };
  }

  // A timestamp represents an instant; localize at display time.
  return { kind: "shift", startsAt: record.startsAt };
}
Enter fullscreen mode Exit fullscreen mode

The important choice is that the date-only branch does not call new Date() or silently apply timezone conversion. In a real application, the downstream rendering adapter also needs to preserve that meaning. I would test the same record across timezones and calendar views, not just test the conversion function in isolation.

The boundary before Bryntum

The services weren't returning one shared event format. We could have mapped each response directly into the calendar, then handled the differences in individual views. I chose a typed application event model instead. It gave IDs, categories, date handling, and permission metadata a consistent shape before anything reached Bryntum.

Where service-specific data stops and calendar rendering begins

SERVICE CONTRACTS
  Shifts · overtime           Absences · days off
  Identifiers and time fields Categories and visibility rules
                 ↓
        Typed application event model
        Stable identity · date handling · categories
        Permission metadata
                 ↓
  Product rules               View adapters
  Normalized events           Day · week · month · team
  Consistent access rules
                 ↓
    Bryntum: event layout, interactions, and presentation
Enter fullscreen mode Exit fullscreen mode

The extra mapping layer meant more code and more tests. The alternative was to maintain the same conversion and visibility logic in several places, including day, week, month, and team views. I preferred one boundary where the behavior could be reviewed and tested.

A responsive calendar still needs fresh data

Calendar navigation creates an awkward balance. People move between weeks and return to ranges they recently viewed. Fetching everything again makes the UI feel slow; never refreshing cached results can leave an old schedule on screen.

Example: moving across weeks, then returning

Step Range Action
01 Week 42 Load the visible range
02 Week 43 Navigate forward
03 Week 42 Reuse cached results; revalidate

We reused loaded ranges, debounced quick navigation, and refreshed in the background when needed. The selected date and view could be restored, but the schedule itself still needed revalidation. Another important check was ensuring a late response from an earlier range could not overwrite the one currently on screen.

What I checked beyond the happy path

A populated calendar isn't proof that its data is right. The cases worth checking were the quiet failures: date-only records shifting days, confidential absence details showing in another view, cached ranges not refreshing, and an old request returning after the user had navigated somewhere else.

What I would carry into the next scheduling project is not a particular calendar configuration. It's the decision to make dates, permissions, and data freshness explicit before rendering. That gives the UI library less to guess about, and gives the rest of the application rules that can be tested without opening a calendar.


Originally published on nehamangla.com.Explore more of my engineering work and articles on my portfolio.

Top comments (0)