Date picker

Free · MITReact Native · Expo

Inline month calendar: months slide with the finger, the selection circle travels from day to day and stretches toward the new one, a range sweeps its band in from the start day, and booked days are crossed out. A compact wheel variant picks day, month and year in the locale's order.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/date-picker.json

Then run npx expo install --fix so react-native-gesture-handler, react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/date-picker.

Usage

import { useState } from "react";
import { DatePicker } from "@/components/goodui-native/date-picker/date-picker";

export function DeliveryDay() {
  const [day, setDay] = useState<Date | null>(null);
  const today = new Date();
  return (
    <DatePicker
      accessibilityLabel="Delivery day"
      value={day}
      onValueChange={setDay}
      min={today}
      max={new Date(today.getFullYear(), today.getMonth() + 2, 0)}
      isDateDisabled={(d) => d.getDay() === 0}
    />
  );
}

When to use

Use it for
  • Picking a day where the weekday and the shape of the month matter: appointments, deliveries, reservations.
  • Picking a stay or a period: use DateRangePicker; booked days block a range from spanning them.
  • Dates far from today such as a birthday: DatePicker variant="wheel".
Reach for something else
  • Choosing a day within the next week or two above a schedule: use week-strip.
  • Choosing a time: use time-slots for bookable times or wheel-picker for any time.
  • Typing a known date quickly (card expiry): use text-field with a mask.

Examples

Stay with booked nights

<DateRangePicker
  accessibilityLabel="Stay"
  value={stay}
  onValueChange={setStay}
  min={new Date()}
  isDateBooked={(d) => bookedKeys.has(d.toDateString())}
/>

Birthday wheel in German order

<DatePicker variant="wheel" locale="de-DE" defaultValue={new Date(1990, 4, 17)} max={new Date()} onValueChange={setBirthday} />

Days with events and a Monday start

<DatePicker weekStartsOn={1} isDateMarked={(d) => hasEvents(d)} value={day} onValueChange={setDay} />

API reference

DatePicker

PropTypeDefault
min
Earliest selectable day. Earlier days are disabled and the calendar does not page before its month.
Date—
max
Latest selectable day. Later days are disabled and the calendar does not page past its month.
Date—
isDateDisabled
Days that cannot be chosen, for example closed Sundays. Shown dimmed.
(date: Date) => boolean—
isDateBooked
Days that are taken, for example fully booked. Shown crossed out, cannot be chosen, and a range cannot span them.
(date: Date) => boolean—
isDateMarked
Days with something on them get a dot under the number.
(date: Date) => boolean—
locale
BCP 47 locale for month and weekday names, digits and the first day of the week. Defaults to the GooduiLocaleProvider's language, else the device locale.
string—
weekStartsOn
First column: 0 Sunday to 6 Saturday. Defaults to the locale's first day of the week.
0 | 1 | 2 | 3 | 4 | 5 | 6—
defaultMonth
Month shown first when nothing is selected. Defaults to the selected date's month, else this month.
Date—
onMonthChange
Called with the first day of the month after paging.
(month: Date) => void—
haptic
`false` silences the select haptic on a day and the warning on a refused range.
boolean—
accessibilityLabel
Names the calendar for screen readers, for example "Check-in date".
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<DatePickerLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
value
Selected day (controlled). `null` for none.
Date | null—
defaultValue
Starting day when uncontrolled.
Date | null—
onValueChange
Called with the chosen day (time set to midnight local).
(date: Date) => void—
variant
`calendar` is an inline month grid. `wheel` is a compact day, month and year wheel in the locale's order, for birthdays and dates far from today.
"calendar" | "wheel""calendar"

DateRangePicker

PropTypeDefault
min
Earliest selectable day. Earlier days are disabled and the calendar does not page before its month.
Date—
max
Latest selectable day. Later days are disabled and the calendar does not page past its month.
Date—
isDateDisabled
Days that cannot be chosen, for example closed Sundays. Shown dimmed.
(date: Date) => boolean—
isDateBooked
Days that are taken, for example fully booked. Shown crossed out, cannot be chosen, and a range cannot span them.
(date: Date) => boolean—
isDateMarked
Days with something on them get a dot under the number.
(date: Date) => boolean—
locale
BCP 47 locale for month and weekday names, digits and the first day of the week. Defaults to the GooduiLocaleProvider's language, else the device locale.
string—
weekStartsOn
First column: 0 Sunday to 6 Saturday. Defaults to the locale's first day of the week.
0 | 1 | 2 | 3 | 4 | 5 | 6—
defaultMonth
Month shown first when nothing is selected. Defaults to the selected date's month, else this month.
Date—
onMonthChange
Called with the first day of the month after paging.
(month: Date) => void—
haptic
`false` silences the select haptic on a day and the warning on a refused range.
boolean—
accessibilityLabel
Names the calendar for screen readers, for example "Check-in date".
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<DatePickerLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—
value
Selected range (controlled). `end` is null while the second day is still to be picked.
DateRange—
defaultValue
Starting range when uncontrolled.
DateRange—
onValueChange
Called after each tap: first with `{ start, end: null }`, then with both days.
(range: DateRange) => void—
rangeBlockedMessage
Text announced and shown nowhere else when a range would span a booked or disabled day. Same as `labels.rangeBlocked`.
string—

Motion and haptics

  • Months sit side by side and follow a horizontal drag 1:1, with resistance before min and past max; release pages one month by position and velocity on the smooth spring. The arrows page on the same spring.
  • Single: one accent circle travels from day to day. The edges facing the new day lead on the snappy spring and the trailing edges follow on smooth, so it stretches toward the target and settles. Each number turns to the on-accent color by how much the circle covers it. A circle appearing in a month grows from 40 % on the morph spring.
  • Range: start and end circles grow in on the morph spring and, when the end is picked, the band sweeps from the start day to the end day across week rows on the smooth spring.
  • Reduced motion: paging with the arrows becomes an instant change with a short fade, a swipe settles with a short timing, the circle jumps (a newly shown one fades in), end circles and the band are simply there.
  • Haptic: select when a day is picked
  • Haptic: warning when a range is refused because it spans an unavailable day
  • Haptic: haptic={false} silences both
  • Only the visible month and its two neighbours are mounted; a far jump lands one month short and pages in.
  • The paging position, the travelling circle, number colors and the band run on the UI thread; React renders when the month or the selection changes, never per frame.
  • Pass stable functions for isDateDisabled, isDateBooked and isDateMarked when they are expensive: they run for every day of the three mounted months on each render.

Accessibility

  • Every day is a button whose label starts with its number, then the full date from Intl, then states: today, fully booked or unavailable, start of range, end of range, in range, has events. accessibilityState carries selected and disabled.
  • On the web days are buttons with aria-pressed and aria-disabled. One day is focusable at a time: arrows move by a day or a week, Page Up and Page Down by a month, Home and End to the week's ends, Enter or Space picks; moving past the month pages the calendar.
  • The month title is a header and a polite live region, so paging is announced on Android. Previous month and Next month are icon buttons with labels and disable at min and max.
  • A range that would span a booked or disabled day is refused with a warning haptic and an announcement (rangeBlockedMessage), and the tapped day starts a new range.
  • Weekday names above the grid are hidden from screen readers; each day's label already names its weekday. Months off screen are hidden from screen readers.
  • Cells are 44 pt tall and a seventh of the width.
  • The wheel variant is three adjustable wheels (Day, Month, Year) in a group named by accessibilityLabel.

Notes for AI

  • DatePicker value is Date | null; DateRangePicker value is { start, end } and onValueChange fires on both taps (end is null after the first). Tapping the start day again clears the range.
  • Dates come back at local midnight. Compare days with toDateString() or year, month and date, not getTime().
  • Month and weekday names, digits and the first weekday come from Intl with the locale prop; do not pass translated arrays. weekStartsOn overrides the first weekday.
  • isDateBooked crosses a day out and blocks ranges across it; isDateDisabled dims it; min and max also stop paging.
  • variant="wheel" exists on DatePicker only and ignores isDateBooked and isDateMarked.