Date range & multiple dates
Pick a check-in / check-out date range, several ranges, or several separate dates, in React Native. DatePicker (and Calendar) switch selection model with mode. TypeScript narrows value and onChange for each mode.
mode | value | onChange | Extra props |
|---|---|---|---|
'single' (default) | Date | null | (date: Date) => void | β |
'range' | DateRange ({ start: Date | null; end: Date | null }) | (range: DateRange) => void | presets, presetsProps, minRangeDays, maxRangeDays, allowSingleDayRange |
'multiple' | Date[] | (dates: Date[]) => void | maxSelections |
'ranges' | DateRange[] | (ranges: DateRange[]) => void | maxRanges, minRangeDays, maxRangeDays, allowSingleDayRange |
The modes are also exported as constants: mode={CALENDAR_MODE.ranges}.
Try the propsβ
modemaxRangeDaysmaxSelectionsmaxRanges<DatePicker
value={value}
onChange={setValue}
mode="range"
maxRangeDays={14}
maxSelections={5}
maxRanges={3}
/>
Rangeβ
<> <DatePicker mode="range" value={range} onChange={setRange} minDate={new Date()} /> <Text style={{ textAlign: 'center', color: '#6E685E' }}> {range.start ? formatDate(range.start) : 'Check-in'} β{' '} {range.end ? formatDate(range.end) : 'Check-out'} {nights ? ` Β· ${nights} night${nights > 1 ? 's' : ''}` : ''} </Text> </>
How taps workβ
- First tap sets the start:
onChange({ start: day, end: null }). - Second tap on or after the start sets the end:
onChange({ start, end: day }). - A tap before the start, or any tap after a complete range, starts a new range from that day.
The band between start and end uses primaryMuted; the ends use primary.
Ranges never cross disabled daysβ
If the days between start and the tapped day include a disabled one (from minDate/maxDate, isDateDisabled or markedDates), the tap starts a new range instead of producing a range that spans an unavailable night. That's exactly what booking flows need:
<DatePicker mode="range" value={range} onChange={setRange} minDate={today} isDateDisabled={(d) => booked.has(toDateKey(d))} />
Minimum and maximum lengthβ
Length is measured in days between start and end (nights): a range from the 10th to the 13th is 3.
| Prop | Effect when violated |
|---|---|
minRangeDays | A second tap that would be too short is ignored; the start stays selected |
maxRangeDays | A second tap that would be too long starts a new range from that day |
allowSingleDayRange={false} | Tapping the start again is ignored (by default it makes a one-day range, start === end) |
<> <Text style={{ textAlign: 'center', marginBottom: 4, color: '#6E685E' }}> Stay 2 β 7 nights </Text> <DatePicker mode="range" value={range} onChange={setRange} minDate={new Date()} minRangeDays={2} maxRangeDays={7} allowSingleDayRange={false} /> </>
Presetsβ
presets adds a row of chips above the days, and each one picks a whole range in one tap: the shortcuts of a report or a filter screen. getRangePresets() returns the usual ones, counted back from today: Today, Yesterday, Last 7 days, Last 30 days, This month and Last month. The chip that matches the range shows selected, so picking the same days by hand selects it too.
presetsProps takes any HorizontalList prop for the row except items, value, onChange and isEqual, which come from the presets and the range. Here showArrows adds βΉ βΊ buttons that scroll the chips, floating over the edges with arrowPosition: 'overlay':
<DatePicker mode="range" value={range} onChange={setRange} presets={presets} presetsProps={{ variant: 'chips', showArrows: true, arrowPosition: 'overlay', accessibilityLabel: 'Quick date ranges', }} maxDate={new Date()} />
variant: 'segmented' puts a few presets on a track instead, and renderItem and styles draw your own chip.
A preset the calendar wouldn't let you pick is disabled: one outside minDate / maxDate, one that crosses a disabled day or breaks minRangeDays, maxRangeDays or allowSingleDayRange. Picking one shows the month its range starts in.
Write your own as { label, start, end }, such as the next weekend, or to label them in another language:
const presets = [
{ label: 'This weekend', start: saturday, end: addDays(saturday, 1) },
...getRangePresets().filter((preset) => preset.label !== 'Yesterday'),
];
DatePickerSheet and DatePickerField take presets and presetsProps too.
Range stylingβ
<DatePicker mode="range" value={range} onChange={setRange} theme={{ colors: { primary: '#E11D48', primaryMuted: '#FFE4E6' } }} styles={{ selectedDay: { borderRadius: 10 }, rangeBand: { borderRadius: 0 }, }} />
Multipleβ
Tap days to toggle them. onChange always receives the dates sorted ascending. With maxSelections, further taps on unselected days are ignored once the limit is reached (deselecting still works).
<> <DatePicker mode="multiple" value={dates} onChange={setDates} maxSelections={5} minDate={new Date()} /> <Text style={{ textAlign: 'center', color: '#6E685E' }}> {dates.length ? dates .map((d) => formatDate(d, undefined, { day: 'numeric', month: 'short' }) ) .join(', ') : 'Pick up to 5 days'} </Text> </>
Multiple rangesβ
mode="ranges" picks several date ranges, e.g. blackout periods or trip legs. Each range is built with two taps, just like range mode. Tapping any day of a finished range removes it. Ranges never overlap, and onChange receives them sorted by start. The range rules (minRangeDays, maxRangeDays, allowSingleDayRange) apply to each range. With maxRanges, taps that would start another range are ignored once the limit is reached.
<> <DatePicker mode="ranges" value={ranges} onChange={setRanges} maxRanges={3} minDate={new Date()} /> <Text style={{ textAlign: 'center', color: '#6E685E' }}> {ranges.length ? ranges.map((r) => `${short(r.start)} β ${short(r.end)}`).join(', ') : 'Pick up to 3 ranges'} </Text> </>
Uncontrolledβ
<DatePicker mode="range" defaultValue={{ start: new Date(), end: null }} onChange={(r) => console.log(r)} />
<DatePicker mode="multiple" defaultValue={[]} onChange={(d) => console.log(d)} />
In a sheetβ
DatePickerSheet disables Done until the range is complete (both start and end). In ranges mode, it waits until no range is half-picked.
<DatePickerSheet
visible={open}
onClose={() => setOpen(false)}
title="Your stay"
mode="range"
value={range}
onConfirm={setRange}
minDate={new Date()}
minRangeDays={1}
/>
β DatePickerSheet
In a form fieldβ
DatePickerField takes the same props without visible, and shows the confirmed range (Oct 5, 2026 β Oct 9, 2026) or dates in the field. Switch presentation in the panel: auto opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
presentationmode<DatePickerField
label="Your stay"
placeholder="Add dates"
value={stay}
onConfirm={setStay}
mode="range"
/>
β Picker fields
Working with rangesβ
The date utilities make common range checks one-liners:
import { isBetweenDays, isSameDay, compareDay } from '@elik56a/react-native-pickers';
const nights = Math.round((range.end!.getTime() - range.start!.getTime()) / 86_400_000);
const includesToday = isSameDay(range.start, new Date()) || isBetweenDays(new Date(), range.start, range.end);
const isFuture = compareDay(range.start!, new Date()) > 0;
Propsβ
All calendar props apply. Mode-specific props:
| Prop | Mode | Type | Default | Description |
|---|---|---|---|---|
presets | range | DateRangePreset[] | β | Chips above the days that pick a whole range, such as getRangePresets(). A preset the rules wouldn't allow is disabled. |
presetsProps | range | RangePresetsListProps | β | HorizontalList props for the presets row, except items, value, onChange and isEqual. Default: chips. |
minRangeDays | range, ranges | number | β | Minimum nights (end β start in days). Shorter second taps are ignored. |
maxRangeDays | range, ranges | number | β | Maximum nights. Longer second taps start a new range. |
allowSingleDayRange | range, ranges | boolean | true | Allow start === end. |
maxSelections | multiple | number | β | Maximum number of selected days. |
maxRanges | ranges | number | β | Maximum number of ranges. |
Relatedβ
- DatePicker: the calendar these modes build on
- Picker sheets: a range in a sheet with Cancel / Done
- Hotel booking range: check-in and check-out with prices on the days