# Build a Date Range Picker with Presets

Source: https://www.techinterview.org/post/3233475319/build-date-range-picker-with-presets/
Updated: 2026-06-30 · techinterview.org

"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) => boolean` prop

- 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-pressed` on 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.
