“Build a date range picker” is a frontend machine-coding problem that punishes ambiguity. Used in analytics dashboards, booking flows, and admin panels. The interview probes calendar math, hover preview, keyboard accessibility, and timezone handling — areas where shortcuts produce subtle bugs.
Clarify scope
- Single picker (one calendar) or dual picker (two side by side)?
- Free-form input, calendar selection, or both?
- Presets like “Last 7 days”, “This month”, “Year to date”?
- Min/max date constraints?
- Disabled dates (booked, blackout)?
- Timezone — user, server, or UTC?
Layout
Common pattern (Stripe / Linear / Notion):
- Two text inputs side by side (start and end)
- Click either opens a popup with two calendars (current and next month)
- Sidebar with presets (“Today”, “Yesterday”, “Last 7 days”, “Last 30 days”, “This month”, “Last month”, “This year”, “Custom”)
- Calendar grid with click-to-pick start, then click-to-pick end
Selection logic
- Click 1: set start, clear end, prepare for hover preview
- Mouse over a future date: preview the range visually (faded background between start and current hover)
- Click 2: set end. If clicked date is before start, swap them.
- Click again after a complete selection: reset and start over
Calendar math
Use a date library (date-fns is the modern default; dayjs is smaller). Avoid native Date arithmetic — DST and month boundaries cause subtle bugs.
import { startOfMonth, endOfMonth, eachDayOfInterval, format } from "date-fns";
function buildMonthGrid(month) {
const start = startOfMonth(month);
const end = endOfMonth(month);
return eachDayOfInterval({ start, end });
}
Hover preview
Track hoverDate in state. Compute the displayed range:
const previewEnd = end ?? hoverDate;
const inRange = (date) =>
start && previewEnd && date >= start && date <= previewEnd;
Style each day cell based on whether it is the start, end, or inside the range. Use isSameDay from your date library for comparison.
Presets
Presets are pure functions: (now) => { start, end }. Examples:
- “Today” — start of today, end of today
- “Last 7 days” — 6 days ago start, today end (inclusive)
- “This month” — start of month, today (or end of month if including future)
- “Last month” — start to end of previous month
The trickiest part is “Last 7 days” — does it include today? Document the choice and stay consistent.
Timezone — the senior signal
Many implementations send local-time strings to the server, which interprets them in UTC, and silent bugs accumulate. Decide explicitly:
- If the server expects UTC: convert user-picked dates to UTC at boundaries (start of day in user TZ, then to UTC)
- If the server stores user-local dates as ISO without timezone: send the local ISO string
- Always document which interpretation you chose
Disabled dates
Booking apps disable already-booked dates and dates before today. Implementation:
isDateDisabled(date) => booleanprop- Style disabled cells visibly different and prevent click
- Hover-preview must skip over disabled dates without breaking the range
Accessibility
- Each day cell as a
<button>with descriptive aria-label (“Saturday, May 11”) - Calendar as a
<table role="grid">with day-of-week headers - Arrow keys navigate days; Page Up/Down navigates months; Home/End to start/end of week
aria-pressedon selected start/end- Live region announces selection: “Range selected: May 1 to May 8”
Mobile considerations
- Single-month layout instead of two on small screens
- Bottom-sheet presentation instead of popover
- Touch-friendly cell size (44px minimum)
- Native date inputs as a fallback option
Edge cases interviewers love
- User selects start, then a preset — does the preset clear the start? Yes, presets are atomic.
- User picks a date in the past with min-date set to today — show inline error
- User’s system timezone changes during selection (rare but possible) — re-render based on new time
- DST transition day — confirm range math handles 23-hour or 25-hour days
- Cross-year ranges — calendar advances correctly
What separates senior from staff
Senior: builds the calendar correctly with date-fns, handles the hover-preview state. Staff: discusses timezone handling explicitly, builds a controlled vs uncontrolled component API, and recognizes the disabled-date case as a hover-preview problem (skipping over).
Frequently Asked Questions
Library options for production?
react-day-picker (most flexible), react-datepicker (older), Mantine DatePicker (with the Mantine ecosystem). For a polished date range picker, react-day-picker with a custom range mode is the modern default.
What about international calendars?
date-fns supports locales for week start, month names, and date formatting. For non-Gregorian calendars (Hijri, Hebrew), you need a different library or custom math.
How do I handle the dual-input parsing?
Parse on blur, not on keystroke. Show inline error if the input does not parse. Format on focus loss to a canonical “MMM d, yyyy” representation.