Skip to content

DateRangePicker

The demo limits ranges to 14 days in 'flag' mode with a confirm step: pick a longer range and the footer shows a warning while Apply stays disabled.

import { DateRangePicker } from '@lunisolar-picker/react/pickers';
<DateRangePicker
aria-label="Report period"
value={range}
onValueChange={(next) => setRange(next)}
max={today}
maxDays={60}
limitMode="flag"
commitMode="confirm"
numberOfMonths={2}
presets={<MyPresets />}
/>;
  • Click a start day, then an end day. Clicking again after a complete range starts a new one.
  • With a mouse or pen, drag across days. Grab either end of an existing range to move it. Hold the pointer at the calendar edge for half a second to turn the month.
  • On touch screens, tap the start and the end. Dragging is off so the page can scroll.
  • In the bottom sheet on phones, one month is shown even with numberOfMonths={2}, so the sheet does not grow past the screen.
  • With the keyboard, press Enter on the start and again on the end.

minDays and maxDays count both ends: 13 to 17 October is 5 days.

limitMode Behaviour
'block' (default) Days that would make the range too long or too short cannot be chosen.
'flag' Any range can be chosen. validity.reasons contains 'range-too-long' or 'range-too-short' and the range gets data-invalid, so you can show your own warning. In 'immediate' mode the invalid range is committed and onValueChange reports the reasons.

In 'confirm' mode the Confirm button stays disabled while the draft range is invalid or incomplete.

Put your own buttons in the presets slot. Inside it, usePickerSelection() returns the working range as draftRange and sets it with setRange. Use validateRange to disable presets that break your limits and isSameRange to highlight the active one:

import { type DateRangeValue, isSameRange } from '@lunisolar-picker/calendar';
import { validateRange } from '@lunisolar-picker/core';
import { usePickerSelection } from '@lunisolar-picker/react/pickers';
function MyPresets({ presets }: { presets: readonly { label: string; range: DateRangeValue }[] }) {
const { draftRange, setRange } = usePickerSelection();
return presets.map((preset) => (
<button
key={preset.label}
type="button"
disabled={!validateRange(preset.range, { maxDays: 60 }).isValid}
aria-pressed={isSameRange(draftRange, preset.range)}
onClick={() => setRange(preset.range)}
>
{preset.label}
</button>
));
}
usePickerSelection()
draftRange The range shown in the calendar, including an unconfirmed draft.
validity Validity of that range.
setRange(range) Show a range. In 'immediate' mode a complete range is committed and the picker closes.
commit(), cancel() Confirm or discard the draft and close.

usePickerSelection works only inside presets and footer. The demo’s warning is a footer:

function LengthWarning() {
const { validity } = usePickerSelection();
if (!validity.reasons.includes('range-too-long')) return null;
return <p role="status">Ranges are limited to 14 days.</p>;
}
<DateRangePicker maxDays={14} limitMode="flag" commitMode="confirm" footer={<LengthWarning />} />;

All DatePicker props except that values are ranges and name is replaced by startName and endName, plus:

Prop Type Default Description
value, defaultValue { start: string; end: string | null } | null null Canonical range.
onValueChange (value: DateRangeValue | null, details: { validity }) => void Called when a range is complete, or on Confirm in 'confirm' mode.
onPick (value: DateRangeValue) => void Called whenever the user completes a range in the calendar.
minDays, maxDays number Length limits, both ends included.
limitMode 'block' | 'flag' 'block' See above.
isOpenEndAllowed boolean false Allow end: null as a committed value, for “no end date”.
presets ReactNode Slot for preset buttons.
startName, endName string Hidden inputs for native form submission.