Interface Design

How a Schedule Interface Works

May 10, 2026

The architecture behind a scheduling system that handles thousands of shifts across hundreds of facilities — daily views, monthly calendars, and the data flow that powers them.

When an administrator opens a scheduling application at 6 AM, they see a deceptively simple interface: a grid of days, each divided into time slots, scattered with colored dots representing shifts. Behind that simplicity lives a system that must answer a single question hundreds of times per second: who is working, where, and when?

The schedule is the beating heart of any staffing application. It is the surface through which facilities post shifts, workers get booked, and the entire staffing lifecycle plays out. It ships as two views — the Daily View and the Monthly View — each optimized for a different job.

MONTUEWEDTHUFRISATSUN1234567THU, MAR 4AM SHIFTSarah K. — CNAPM SHIFTUnfilledNOC SHIFTJames R. — RNMONTHLY VIEWSHIFT DETAILS

The Daily View

The Daily View answers the question: “What is happening today?” It shows every shift for a single day, grouped into three columns — one for each time slot. On desktop, a collapsible sidebar provides a mini calendar and qualification filters. On mobile, the same information unfolds as a horizontal carousel with bottom sheets.

This responsive split isn’t a CSS media query trick. The daily view ships two entirely separate component trees: DesktopDailyView and MobileDailyView, chosen at the container level based on a 750px viewport threshold. Each is purpose-built for its form factor.

SCHEDULEDAYMONTHMARCH 2026QUALIFICATIONSCNARNLVNAM05:00 – 11:59SKSarah K.CNA · 06:00 – 14:00+ Post shiftPM12:00 – 17:59MJMaria J.RN · 14:00 – 22:00NOC18:00 – 04:59UnfilledCNA · 22:00 – 06:00SIDEBARAM COLUMNPM COLUMNNOC COLUMN

Time slots

Healthcare staffing revolves around three shifts. We call them time slots — AM, PM, and NOC (nocturnal). Every shift posted on the platform falls into one of these three buckets based on its start hour.

SlotHoursStart RangeTypical Role
AM05:00 – 11:59MorningDay shift nurses, CNAs
PM12:00 – 17:59AfternoonEvening coverage
NOC18:00 – 04:59NightOvernight staff

The function getTimeSlotFromHour() is the single source of truth for this mapping. When a shift arrives from the API with a start time of 2026-03-04T14:00:00Z, we extract the hour in the facility’s timezone, and that hour determines the column.

Why timezone matters here: A shift starting at 00:00 UTC could be an evening PM shift in California (4 PM PST) or an early AM shift in London. The slot assignment always happens in the facility’s local timezone, never UTC.

How data flows

The daily view’s data is orchestrated by a single hook: useDailyViewData(). Think of it as a conductor coordinating five parallel API calls, each returning a different piece of the puzzle.

useDailyViewData()WorkplaceWorkerTypesQualifications list:CNA, RN, LVN, STNA…FacilityShiftCountsPer-date summaries:filled: 3, open: 1, total: 4GetDayShiftsFull shift objects:worker, times, state, ratePendingInvitesAvailableWorkersAPI BOUNDARYGET/calendar/facilityGET/calendar/facilityCountgroupShiftsByTimeSlot(){ am: Shift[],pm: Shift[],noc: Shift[] }

The two core API endpoints are /calendar/facility for full shift objects and /calendar/facilityCount for lightweight summaries. The count endpoint is intentionally fast — under 50ms at p99 — because it powers the calendar dot indicators that need to feel instant.

Qualification filtering works through a subtle pattern: rather than sending a list of selected qualifications in one request, we fire a separate API call for each selected type and merge the results. This guarantees accurate counts regardless of how the backend handles multi-type queries.

// Each selected qualification triggers its own query
const queries = selectedTypes.map(type =>
  createFacilityShiftCountsQueryOptions({
    facilityId,
    dateFilter: { start, end },
    tmz: facility.timezone,
    type: type.name,
  })
);

// Results are merged after all queries resolve
const merged = combineShiftCountResults(results);

The Monthly View

If the Daily View is a microscope, the Monthly View is a telescope. It shows the entire month at a glance — a 7-column by 6-row grid of 42 day cells, including padding days from adjacent months. Each cell contains three stacked time slots with dot indicators showing shift presence and fill state.

The Monthly View introduces two posting modes: Bulk Posting and Block Booking. Bulk posting adds incremental shifts across multiple days, while block booking reserves contiguous slot ranges for specific workers. Both use localStorage-backed draft persistence with schema versioning, so accidentally closing the tab doesn’t lose 10 minutes of work.

Making navigation feel instant

The schedule uses an aggressive prefetching strategy. When you’re looking at March 4th, we’re already fetching March 3rd and March 5th in the background. When you hover over the “Month” tab, we start loading the monthly view data before you click.

MAR 3PREFETCHEDMAR 4CURRENT VIEW3 AM · 2 PM · 1 NOCMAR 5PREFETCHEDMONTH VIEWPREFETCH ON HOVERADJACENT DAY PREFETCHVIEW MODE PREFETCH

View transitions between day and month use the CSS view-transition-name API. Combined with prefetched data, the switch feels like flipping a page rather than loading a new one.

function startScheduleViewTransition({ update }) {
  if (!document.startViewTransition) {
    update();
    return;
  }
  document.startViewTransition(() => {
    flushSync(() => update());
  });
}

All navigation state lives in the URL: ?date=2026-03-04&view=day&shiftId=abc123. This means every view is a permalink. You can share a link to a specific shift on a specific day, and the recipient sees exactly what you see. The useShiftDeepLinking() hook parses these parameters on mount and auto-opens the relevant drawers and dialogs. All URL updates use history.replace() rather than push(), so the browser back button takes you out of the schedule entirely rather than cycling through every shift you looked at.

← All chapters