Date Pickers & Date Range Selection
If you're building B2B SaaS in 2026, date pickers are everywhere — analytics filters ("last 30 days"), schedulers, due dates, report ranges, billing cycles, hire dates, expiration. The naive approach: drop in a <input type="date"> and call it done. The structured approach: use a date library (date-fns / Luxon / Day.js — not Moment), pick a UI library (react-aria / react-day-picker / shadcn DatePicker), handle timezones explicitly (see timezone-handling-chat.md), provide preset ranges, accessibility, and locale formatting. This guide covers the implementation craft of date pickers themselves; for time-zone math, see the companion guide.
1. Pick a date library — Moment is dead
Choose a date manipulation library for 2026.
Don't use Moment.js:
- 76KB minified (massive)
- Mutable API (footguns)
- Officially in maintenance mode since 2020
- Tree-shaking impossible
Modern options:
date-fns (recommended default):
- Functional API (immutable)
- Tree-shakeable (only import what you use)
- ~1-15KB depending on functions used
- Locale-aware
- TypeScript native
Day.js:
- Moment-compatible API (easy migration)
- ~7KB total
- Plugin system for advanced features
- Good for projects migrating from Moment
Luxon:
- TypeScript-first
- Strong timezone support (Intl-based)
- ~75KB but features-rich
- Best for heavy timezone needs (see timezone-handling-chat)
Temporal (TC39 proposal):
- Built-in browser API (no library needed)
- Available natively in modern browsers (2025+)
- Use polyfill for older support
- The future; consider for new projects
For 2026 default: date-fns + Temporal-aware code.
For timezone-heavy: Luxon or Temporal directly.
For Moment migrations: Day.js.
Output:
1. Recommendation
2. Bundle-size budget
3. Sample import (tree-shaken)
4. Locale support strategy
5. Migration path if currently using Moment
The 2026 trend: Temporal is landing in browsers. New projects should plan for Temporal as the long-term default; date-fns/Luxon as bridge.
2. Pick a date-picker UI library
Choose a date-picker UI library for React in 2026.
Native HTML5 (<input type="date">):
- Pros: zero deps; works
- Cons: ugly cross-browser; broken on iOS Safari edge cases; minimal range/preset support
- Verdict: only for ultra-simple form fields
react-day-picker:
- Lightweight (~10KB); accessible by default; range + multi support
- Used by shadcn/ui's Calendar component
- Recommended default in 2026
shadcn/ui DatePicker:
- Built on react-day-picker + Radix Popover
- Tailwind-styled
- Copy-paste into project; full control
- Recommended for shadcn-based projects
react-aria DatePicker (Adobe):
- Accessibility-first
- Composable
- Slightly higher learning curve
Mantine DatePicker / Chakra DatePicker / Material UI DatePicker:
- Bundled with parent UI libraries
- Use whichever matches your design system
react-datepicker (legacy popular):
- 700K weekly downloads
- Mature but less modern API
- Locked styling (CSS)
- OK alternative; not first choice
For 2026:
- shadcn-based stack → shadcn Calendar (uses react-day-picker)
- Custom design system → react-day-picker directly
- Adobe / accessibility-first → react-aria
- Material/Mantine → use library's DatePicker
Output:
1. Recommendation
2. Install + setup
3. Styling integration
4. Single date + range example
5. Bundle-size addition
The 2026 default: shadcn/ui Calendar component. Built on react-day-picker, Tailwind-styled, accessible. Most teams should start there.
3. Single date vs date range — different UX
The most-shipped pattern: single date is a popover with calendar; range is two-month side-by-side calendar.
Build single-date and range pickers.
Single date picker:
- Trigger: input or button showing formatted date
- Click: opens popover with month calendar
- Click date: closes popover; sets value
- Today shortcut + clear button
- Keyboard: arrows to navigate, enter to select
Range picker:
- Trigger: input showing "Start - End"
- Click: opens popover with TWO months side-by-side
- Click first date: sets start; live-preview range as user moves
- Click second date: sets end; closes popover
- Smart logic: if user clicks earlier date second, swap start/end
- Reset on opening if both already set
Preset ranges (highly recommended for analytics):
- Today, Yesterday, Last 7 days, Last 30 days, This month, Last month, This quarter, This year, Custom
- Click preset → set range + close (or stay open with range visible)
- Custom = user picks dates
Comparison ranges (advanced):
- Compare to "previous period" / "previous year"
- Two ranges visible
- Often paired with line charts
For [USE CASE], output:
1. Picker type (single / range / range with preset / range with comparison)
2. Component implementation
3. Default value strategy
4. Placeholder text
5. Validation (required, min, max)
The preset rule: if your picker is for analytics filtering (last 30 days etc.), 80% of users click a preset, not custom. Make presets prominent — vertical menu on left of calendar.
4. Timezone awareness in date pickers
A common bug: user picks "Mar 15" in their timezone, server stores midnight UTC, user sees "Mar 14" tomorrow.
Handle timezones in date pickers.
Three patterns based on data semantics:
Pattern 1: "Local date" (no time component)
- Birthday, hire date, due date — date with no time
- Store as YYYY-MM-DD string (no timezone)
- Display same string everywhere
- Don't convert to UTC midnight; that introduces the bug
Pattern 2: "Date + time at user's location"
- Meeting at 3pm Tuesday — user's local time
- Store as ISO 8601 with timezone (2026-03-15T15:00:00-07:00)
- Display in user's local time
- Convert when other timezone users view
Pattern 3: "Universal moment in time"
- Created_at, login_at — server timestamps
- Store as UTC (epoch ms or ISO Z)
- Display in user's local time (using their browser locale)
Date picker implementation per pattern:
- Pattern 1: pure date picker (no time); send YYYY-MM-DD
- Pattern 2: date + time picker with explicit timezone selector
- Pattern 3: typically server-set; user only filters by date range
Common bug: using new Date(string) in JavaScript
- "2026-03-15" parsed as UTC midnight
- new Date(...).toLocaleDateString() in PST shows "Mar 14"
- Fix: parse "YYYY-MM-DD" without timezone offset (use date-fns parseISO with care)
For [USE CASE], output:
1. Which pattern fits
2. Storage format (string / ISO / epoch)
3. Picker config (with/without time)
4. Timezone display strategy
5. Test cases for DST + timezone edge cases
The "birthday bug": classic. User in PST enters Mar 15 birthday. Server stores 2026-03-15T00:00:00Z. User in JST sees Mar 15 9am (correct date). User in HST sees Mar 14 2pm (wrong date). Pattern 1 (date string) avoids this.
5. Locale-aware formatting
Date formats vary by country. Plan for it.
Implement locale-aware date formatting.
Common formats by locale:
- en-US: 03/15/2026 (MM/DD/YYYY) — confusing internationally
- en-GB: 15/03/2026 (DD/MM/YYYY)
- de-DE: 15.03.2026 (DD.MM.YYYY)
- ja-JP: 2026/03/15 (YYYY/MM/DD)
- zh-CN: 2026年3月15日
Browser API:
- Intl.DateTimeFormat (built-in; no library)
- new Intl.DateTimeFormat('en-US').format(date) → "3/15/2026"
- Use 'short', 'medium', 'long', 'full' presets
Best practice for B2B SaaS:
- Default to user locale (browser)
- Allow override in user preferences
- Display ISO-ish format (YYYY-MM-DD) for unambiguous UI
- Use long format for user-facing ("March 15, 2026") to avoid ambiguity
Picker considerations:
- Calendar week-start: Sunday (US) vs Monday (most of world)
- Month names localized
- Date format displayed in input matches locale
Output:
1. Locale-detection strategy
2. User-preference override
3. Intl.DateTimeFormat usage
4. Picker config: weekStart, locale prop
5. Test in locales: en-US, en-GB, de-DE, ja-JP
The MM/DD/YYYY trap: Americans write 3/15/2026; Europeans write 15/3/2026. "5/3/2026" is ambiguous. Use ISO (2026-03-15) or long form (March 15, 2026) for user-facing dates that cross locales.
6. Validation — min, max, disabled days, custom rules
Validate date input.
Common validations:
- Required (date must be set)
- Min date (no past dates / no dates before another field)
- Max date (no future dates / no dates after another field)
- Disabled specific dates (holidays, weekends, fully-booked days)
- Date range validation (start < end; minimum range; maximum range)
Custom rules:
- Business days only (Mon-Fri)
- Working calendar (factor in office holidays)
- Available slots from API (backend determines)
UI patterns:
- Disabled dates greyed out, can't click
- Tooltip on disabled: "Office closed" / "Slot full"
- Inline error: "End date must be after start date"
- Async availability check (debounced)
Implementation:
- React Hook Form + Zod for schema validation
- date-fns helpers: isWeekend, isWithinInterval, eachDayOfInterval
- Async validation for slot availability (with loading state)
Output:
1. Validation schema (Zod or react-hook-form rules)
2. Disabled-dates function for picker
3. Server-side mirror validation
4. Error message UX (inline + sticky)
5. Async-availability pattern with debounce
The min/max trap: setting min={today} on a date picker doesn't prevent server submission of past dates. Always validate server-side as well.
7. Accessibility — keyboard, screen reader, focus
Date pickers are accessibility hard mode. Don't skip.
Make date pickers accessible.
Keyboard requirements:
- Tab: focus the input/trigger
- Enter / Space / Down arrow: open popover
- Escape: close popover
- Arrow keys: navigate days
- Page Up / Page Down: previous / next month
- Shift + Page Up / Down: previous / next year
- Home / End: first / last day of week
- Enter: select focused date
Screen reader requirements:
- Trigger announces "Date picker, Mar 15 2026 selected"
- Calendar announces month/year on open
- Each day announces "Tuesday, March 15, 2026" with availability status
- Selected date announces "selected"
- Disabled dates announce "unavailable"
- Range mode announces start vs end clearly
Focus management:
- On open: focus moves to selected date or today
- On close: focus returns to trigger
- Within calendar: arrow keys move focus visually + programmatically
Libraries that handle this well:
- react-aria DatePicker (Adobe) — best-in-class
- react-day-picker — good with proper config
- shadcn Calendar — inherits react-day-picker
Test:
- VoiceOver (Mac) + NVDA (Windows)
- Keyboard-only navigation
- axe-core / Lighthouse audit
Output:
1. Library-specific accessibility config
2. ARIA attributes (aria-haspopup, aria-expanded, aria-label)
3. Keyboard handler matrix
4. Screen-reader announcements
5. Test plan
The skipped-accessibility cost: form date pickers that fail keyboard nav block screen-reader users from completing forms. WCAG / Section 508 violations. react-aria solves this; rolling your own is risky.
8. Time picker (when date alone isn't enough)
Date + time pickers are harder. Plan separately.
Implement date + time picker.
Two patterns:
Pattern A: Combined picker
- One popover: calendar above, time selector below
- Used in scheduling tools (Calendly-like)
- More complex but feels integrated
Pattern B: Separate inputs
- Date input + time input, side by side
- Simpler implementation
- More flexibility (user can change one without other)
Time selector options:
- Native <input type="time"> (works; ugly cross-browser)
- Hour + minute dropdowns (granular)
- Slider (rare; for ranges)
- 24-hour vs 12-hour AM/PM (locale-dependent)
- Step intervals (every 15 min, every 30 min, every hour)
Specific to scheduling:
- Show only available slots (from server)
- Slot duration (30 min, 60 min)
- Buffer between slots
- Working hours (8am-6pm)
- Time zone disclosure
For [USE CASE], output:
1. Combined or separate
2. Time-input strategy (native / dropdowns / slider)
3. 12 vs 24 hour
4. Step interval
5. Slot-availability integration
6. Timezone handling
The combined-picker complexity: most teams underestimate. Date alone is hard; date + time is much harder. If you can decompose (date input, time input separately), do it.
9. Mobile — native pickers vs custom
Mobile date pickers: use native or custom?
Decide mobile date-picker approach.
Native (<input type="date">):
- iOS: scrolling wheel, OS-controlled UX
- Android: Material picker, OS-controlled UX
- Pros: familiar; no custom code; respects user settings
- Cons: can't customize (presets, ranges); inconsistent with desktop UX
Custom on mobile (same as desktop):
- Pros: brand-consistent; presets work; design control
- Cons: more code; need to nail mobile UX (touch targets, sheet from bottom)
Hybrid (recommended):
- Desktop: custom popover with calendar
- Mobile: bottom sheet with native-feeling calendar (large touch targets)
- Or: use native input on mobile, custom on desktop
Touch considerations:
- Day cells minimum 44x44px (iOS HIG)
- Bottom sheet pattern (slide up from bottom)
- Confirm/cancel buttons (don't auto-close on date click on mobile)
- Year picker (tap year, see scrollable list)
Library support:
- react-day-picker: scales OK to mobile
- react-aria: handles mobile gracefully
- shadcn Calendar: works on mobile but tight
Output:
1. Mobile strategy (native / custom / hybrid)
2. Bottom-sheet pattern (if custom)
3. Touch target sizing
4. Confirm/cancel pattern
5. Test on iOS Safari + Android Chrome
The hybrid pattern works well: same date library, different presentation. Desktop popover; mobile bottom-sheet.
10. Common patterns — analytics, scheduling, due-date, expiration
Date picker patterns by use case.
Pattern 1: Analytics filter
- Range picker with presets prominent (Last 7 / 30 / 90 days, MTD, QTD, YTD, custom)
- "Compare to" toggle (previous period / previous year)
- Default: last 30 days
- URL state: serialize as ?from=2026-03-01&to=2026-03-31
Pattern 2: Scheduling / appointment
- Single date + time picker
- Available slots from server
- Working hours filtering
- Timezone disclosure
- Buffer between slots
Pattern 3: Due date
- Single date picker
- Today / Tomorrow shortcuts
- "+1 week" / "+1 month" relative options
- Optional: time component for deadlines
- Default: empty or +1 week
Pattern 4: Expiration / valid-until
- Single date picker
- Future-only (min = today)
- Common shortcuts: "1 month", "3 months", "1 year"
- Display countdown: "Expires in 14 days"
Pattern 5: Hire / start date
- Single date (no time)
- Min: today (or onboarding date)
- No timezone (Pattern 1 from above)
Pattern 6: Birthday
- Single date (no time, no timezone)
- Year scrolling for older years
- Allow no year (just month/day) for some flows
For [USE CASE]: which pattern + library config + UX details.
The analytics-pattern detail: include "Compare to previous period" toggle from day one. Once your dashboards have it, users find it indispensable. Adding later breaks URL backward compat.
What Done Looks Like
A v1 date-picker system for B2B SaaS in 2026:
- date-fns or Day.js (not Moment) for date math
- shadcn Calendar / react-day-picker / react-aria DatePicker for UI
- Single date and range pickers as needed
- Preset ranges for analytics filters
- Locale-aware formatting (Intl.DateTimeFormat)
- Timezone handling matched to data semantics (Pattern 1/2/3)
- Keyboard navigation + screen-reader support
- Min / max / disabled-days validation client + server
- Mobile-friendly (bottom sheet or native)
- URL state for filter ranges (analytics)
Add later when product is mature:
- Saved date ranges (user-defined presets)
- Comparison ranges (period-over-period)
- Recurring date selection (every Tuesday)
- Multi-date selection (multiple distinct dates)
- Working-calendar awareness (skip company holidays)
The mistake to avoid: using Moment.js. Bloated, mutable, deprecated. Migrate to date-fns / Day.js / Temporal.
The second mistake: timezone math without thinking. The "birthday bug" hits everyone once. Pick the right pattern (date-only vs datetime-with-tz vs UTC) before storing.
The third mistake: rolling your own date picker. Accessibility is hard; libraries solve it. Use react-day-picker / react-aria; don't build from scratch.
See Also
- Timezone Handling — companion guide for timezone math
- Form Validation UX — date validation patterns
- Multi-Step Forms & Wizards — date inputs in flows
- Internationalization — locale-aware formatting
- Customer Analytics Dashboards — date range filters
- Charts & Data Visualization — date axis formatting
- Schema Validation Zod — date schema validation
- Data Tables: Sort, Filter, Pagination, Bulk Actions — date filter in tables
- VibeReference: Scheduling & Booking APIs — adjacent scheduling integrations
- VibeReference: Components — UI primitives
- VibeReference: Radix UI — Radix Popover for picker triggers
- VibeReference: shadcn/ui — shadcn Calendar component
- LaunchWeek: Onboarding Flow — date inputs in onboarding