Back to Machine Coding
Machine Coding
easy
mid

How would you build a Google Calendar style week view?

Week grid: time-axis vertical (hours), day-columns horizontal. Events absolutely positioned within their day column: `top = startMinutes * pxPerMinute`, `height = duration * pxPerMinute`. Overlapping events use the cluster-and-column layout algorithm to share horizontal space. Multi-day events span the day columns above the grid. Click empty to create, drag to move/resize.

5 min read·~45 min to think through

Implementation of the week-view (see also the system-design version: [[frontend-system-design-design-a-calendar-application-like-google-calendar]]).

1. The grid

ts
+-------+--------+--------+--------+--------+
| Time  |  Mon   |  Tue   |  Wed   |  Thu   |
+-------+--------+--------+--------+--------+
| 8 AM  |        | [Stand-up]      |        |
| 9 AM  | [1:1]  |        | [Demo] |        |
| 10 AM |        |        |        |        |
  • A vertical time axis (1 row per hour or 15 minutes).
  • N day columns (5 or 7), each is a positioning container.
  • Time slots are usually 60-minute rows with sub-grid lines.

2. Event positioning

For a day column of height columnHeight representing 24 hours (pxPerMinute = columnHeight / (24 * 60)):

js
const top = startMinutesFromMidnight * pxPerMinute;
const height = (endMinutes - startMinutes) * pxPerMinute;

Render each event as an absolutely positioned div within its day column.

3. Overlap layout — the core algorithm

js
function layoutDay(events) {
  // events: {start, end, ...}
  events.sort((a, b) => a.start - b.start || b.end - a.end);

  // Group into clusters of mutually-overlapping events.
  const clusters = [];
  let cur = null;
  for (const e of events) {
    if (!cur || e.start >= cur.endMax) {
      cur = { events: [], endMax: e.end };
      clusters.push(cur);
    }
    cur.events.push(e);
    cur.endMax = Math.max(cur.endMax, e.end);
  }

  // Within a cluster, assign each event the leftmost free column.
  for (const cluster of clusters) {
    const cols = [];                       // each col is an array of events
    for (const e of cluster.events) {
      let placed = false;
      for (const col of cols) {
        if (col[col.length - 1].end <= e.start) { col.push(e); e.col = cols.indexOf(col); placed = true; break; }
      }
      if (!placed) { cols.push([e]); e.col = cols.length - 1; }
    }
    const numCols = cols.length;
    for (const e of cluster.events) { e.numCols = numCols; }
  }

  return events;
}

Render:

jsx
<EventBlock style={{
  top, height,
  left: `${(e.col / e.numCols) * 100}%`,
  width: `${(1 / e.numCols) * 100}%`,
}} />

4. Multi-day & all-day events

Render in a separate band above the time grid; spans the relevant day columns. Use a small layout pass that packs overlapping multi-day bars into stacked rows.

5. Interactions

  • Click empty slot → open a "new event" modal pre-filled with that slot's time.
  • Drag on empty → create with computed duration.
  • Drag an event → move; show ghost; on drop, snap to 15-minute grid, optimistically update.
  • Resize handle at bottom → change duration.
  • Snap to 15-min increments; use transform: translate for the drag to avoid layout.

6. Current-time indicator

A horizontal line at now * pxPerMinute in today's column; update once per minute.

7. Timezone

Render times in the user's display timezone; events come in as ISO timestamps. Use date-fns-tz or Temporal.

8. Performance

  • Only render the visible week's events.
  • Memoize the layout result per day.
  • Use transform for drag; requestAnimationFrame for mousemove.
  • Avoid re-laying-out every event on a single move; only the affected day.

9. Accessibility

  • Grid with role="grid", role="row", role="gridcell".
  • Events as <button> with descriptive labels: "Stand-up, Tuesday 9 AM to 9:30 AM".
  • Keyboard: arrows to move focus across slots; Enter to open; Shift+arrows to move the focused event.
  • Live region announces moves and new-event creation.

Interview framing

"Day columns + a vertical time axis. Events are absolutely positioned within their day column using top = startMinutes * pxPerMinute and height = duration * pxPerMinute. Overlapping events need the cluster-and-columns layout: sort by start, group overlapping into clusters, assign each event the leftmost free column, then render with left = col/numCols and width = 1/numCols as percentages. Multi-day events go in a band above the grid. Drag uses transform to avoid layout. Snap to 15 minutes. Render only the visible week; memoize the per-day layout. Accessibility is grid semantics + buttons for events + keyboard nav."

Follow-up questions

  • Walk through the overlap layout algorithm with 3 overlapping events.
  • Why use transform for drag instead of changing top/left?
  • How do you handle multi-day events?
  • How does timezone handling change the math?

Common mistakes

  • No overlap layout — events visually stacked.
  • Animating top/left on drag — layout thrash.
  • Re-layout on every mousemove frame.
  • Hardcoding pixels for time slots — breaks on responsive sizing.

Performance considerations

  • Render only visible week; memoize layout per day; transform-only drag; rAF mousemove; avoid full re-render on every drag tick.

Edge cases

  • Event crossing midnight.
  • DST days (23/25 hours).
  • Many overlapping events squeezing widths.
  • Very short events (5 minutes) — minimum visual height.

Real-world examples

  • Google Calendar week view, Cal.com, Fantastical.
  • react-big-calendar, FullCalendar.

Senior engineer discussion

Seniors implement the overlap layout correctly (a quiet but visible quality signal), use transform for drag, treat timezone as a first-class concern, and make the grid accessible — events are interactive elements, not just decoration.

Related questions