Build a Calendar Component: Date Picker, Range, and Recurrence

“Build a date picker” or “build a calendar component” is a deceptively hard frontend interview question. It exposes whether you understand time zones, locale-aware date handling, the accessibility patterns of grid widgets, and the small UX details that make a date picker feel professional.

Functional requirements

  • Display a month view with selectable days
  • Navigate months (back/forward)
  • Single date selection (or range, or multi-select — pick scope)
  • Disable past dates or specific dates
  • Highlight today
  • Keyboard navigation
  • Screen-reader friendly
  • Locale-aware (week starts Sunday vs Monday, month/day names)

Architecture

Three core pieces:

  1. State: current month, selected date(s)
  2. Layout: 7-column grid with 5–6 rows
  3. Day cell: button with date, selected state, disabled state

Date library

Native Date is treacherous (time zone bugs, immutability, no formatting). Use:

  • date-fns: tree-shakeable, functional, modern
  • Day.js: tiny (2KB), drop-in for moment users
  • Luxon: if you need time zone math

Avoid moment.js — deprecated and large.

Generating the month grid

Algorithm:

  1. Find first day of the month
  2. Find first day of the week containing it (offset back to Sunday or Monday)
  3. Generate 6 × 7 = 42 days from that starting point
  4. Mark days outside the current month as muted

Always render 6 rows — different month lengths fit cleanly.

Time zones

The most-broken aspect of date pickers. Recommended:

  • Calendar shows dates in the user’s local time zone
  • Internal state is a date string (“2026-05-15”) not a Date object
  • When sending to API, convert to ISO string with explicit time zone
  • For events with time, store a separate time field plus the date

Range selection

Two clicks: first sets start, second sets end. Hover preview shows what the range would be if the user clicks now.

Edge cases:

  • User clicks end before start → swap
  • Same-day selection → range of one day
  • Range spans months → both months visible (typically two-pane layout)

Keyboard navigation

The ARIA Authoring Practices recommend:

  • Tab moves into and out of the calendar grid
  • Arrow keys move between days
  • Page Up/Down moves between months
  • Shift + Page Up/Down moves between years
  • Home/End jumps to first/last day of week
  • Enter or Space selects

ARIA

The grid is role=”grid” or role=”application”. Each day is a button with aria-label=”May 15, 2026, Monday.” Today has aria-current=”date.” Selected days have aria-pressed=”true.”

The grid_cell ARIA pattern is detailed but worth implementing for production date pickers.

Locale

Use Intl.DateTimeFormat for month and day names. The user’s locale should drive:

  • First day of week (Sunday in US, Monday in EU)
  • Month names (“May” vs “mai” vs “5月”)
  • Day-of-week abbreviations
  • Date format on display

Common mistakes

  • Storing dates as JavaScript Date objects (timezone issues)
  • Forgetting accessibility (no keyboard nav, no aria)
  • Hardcoding “Sunday” as the first day of week
  • Re-rendering the entire calendar on every keystroke (performance)
  • Not handling DST transitions in time-aware date pickers

Should I use a library?

For production: yes. React Aria Calendar, react-day-picker, or @internationalized/date are battle-tested and accessible. Hand-rolling is for interview answers and learning.

Frequently Asked Questions

How do I handle disabling dates based on availability?

Take a function prop: isDateDisabled(date) → boolean. Each day cell calls it. Memoize for performance with large calendars.

What if the user wants to pick a time as well?

Add a separate time picker below the date grid. Combine into a Date or ISO string when storing.

How do I handle a range that spans years?

Two-pane layout works visually. Internally just two dates. Performance: don’t render years of days; render one or two months at a time.

Scroll to Top