Date picker
Free · MITReact Native · ExpoInline 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/date-picker.jsonThen 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
- 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".
- 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
| Prop | Type | Default |
|---|---|---|
| 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
| Prop | Type | Default |
|---|---|---|
| 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.
Related
A horizontal week of days: one accent pill slides between days and stretches toward the new one, weeks page with the finger 1:1, dots mark busy days, and a header shows the month with a Today button.
A booking grid of time slots grouped into morning, afternoon and evening: the picked slot pops into the accent, taken slots are crossed out, low availability shows "2 left", and a summary with the end time and the confirm button rides under the grid or pinned to the bottom of the screen.
iOS-style wheel: rows sit on a cylinder in 3D perspective, follow the finger 1:1, keep the fling's momentum and settle exactly on a row with a select tick for every row that passes. Compose several into one picker with WheelPickerGroup. On the web it scrolls with the mouse wheel and drags with the mouse.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.