DateRangePicker
date-range-pickerSelects a start and end date from a dual-month range calendar.
Usage
Basic usage
Click the trigger to pop up the bi-monthly calendar, select the starting and ending ends in order to determine the range.
<DateRangePicker defaultValue={["2026-06-08", "2026-06-20"]} />Controlled
The external controlled value is [start, end] (ISO YYYY-MM-DD), clear and return null.
const [range, setRange] = useState<DateRangeValue | null>(["2026-06-08", "2026-06-20"]);
<DateRangePicker value={range} onValueChange={setRange} />No quick preset
presets={false} Hide the "Today/Last 7 Days..." default column on the left.
<DateRangePicker defaultValue={["2026-06-03", "2026-06-09"]} presets={false} />Limited range + disabled weekends
minDate / maxDate frame the optional range, and disabledDate further prohibits certain days.
<DateRangePicker
defaultValue={["2026-06-10", "2026-06-12"]}
minDate="2026-06-01"
maxDate="2026-06-30"
disabledDate={(iso) => {
const day = new Date(iso + "T00:00:00").getDay();
return day === 0 || day === 6;
}}
/>Month range and year range
picker sets the granularity, with the same meaning as the prop of the same name on DatePicker (the equivalent of el-date-picker monthrange). Values become ["YYYY-MM"] or ["YYYY"] and the presets switch to the common options for that granularity. The component still orders the two ends itself, so a start later than the end cannot be produced.
<DateRangePicker picker="month" defaultValue={["2026-03", "2026-06"]} />
<DateRangePicker picker="year" defaultValue={["2024", "2026"]} />An upper bound on a month range
maxDate always speaks in ISO dates. At month granularity a month is disabled only when all of it is out of bounds, so passing today gives you "current month selectable, future months greyed out" -- which is what stops an operator from picking next year's month on the right-hand panel.
<DateRangePicker picker="month" maxDate="2026-08-14" defaultValue={["2026-05", "2026-08"]} />Size
size uses the same scale as Input and Select (sm 32px / md 40px / lg 48px), so controls sitting on one form row line up. Date-cell geometry inside the panel does not change with it.
<DateRangePicker size="sm" defaultValue={["2026-06-08", "2026-06-20"]} />
<DateRangePicker size="md" defaultValue={["2026-06-08", "2026-06-20"]} />
<DateRangePicker size="lg" defaultValue={["2026-06-08", "2026-06-20"]} />Disabled
The whole page is grayed out and the trigger cannot be opened.
<DateRangePicker defaultValue={["2026-06-01", "2026-06-15"]} disabled />When to use
Use DateRangePicker to select a start and end with two panels shown side by side and quick presets. picker sets the granularity -- day (default), month, or year -- matching el-date-picker's daterange, monthrange, and yearrange. Use DatePicker for one date, DateTimePicker for date and time, or Calendar for an always-visible month panel. The HulianUI date family has no external date-picker dependency and uses fixed-width string values.
Import
import { DateRangePicker } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| value | [string, string] | null | - | Controlled value [start, end]; the shape follows picker (YYYY-MM-DD / YYYY-MM / YYYY); null = cleared; controlled when passed in |
| defaultValue | [string, string] | null | - | uncontrolled initial value |
| picker | "date" | "month" | "year" | "date" | Selection granularity, same meaning as the prop of the same name on DatePicker. The panels follow: two month calendars, two year pages (12 months each), or two 12-year pages. |
| size | "sm" | "md" | "lg" | "md" | Trigger size, on the same scale as Input (32 / 40 / 48px). Date-cell geometry inside the panel does not change with it. |
| minDate | string | - | The earliest selectable date. Always an ISO YYYY-MM-DD string, regardless of `picker`; at month and year granularity a cell is disabled only when the whole month or year is out of bounds. |
| maxDate | string | - | The latest selectable date, same convention as minDate |
| disabledDate | (isoDate: string) => boolean | - | Custom disabling. The argument is always an ISO YYYY-MM-DD string; at month and year granularity it is asked once per cell, with the first day of that month or year. |
| presets | boolean | DateRangePreset[] | true | true or omitted uses the defaults for that granularity (day: Today / Last 7 days / Last 30 days / This month; month: This month / Last 3 months / Last 6 months / This year; year: This year / Last 3 years / Last 5 years), all resolved through the active locale. Pass an array for custom presets or false to hide them. |
| placeholder | [string, string] | follows picker | Placeholder pair [start, end]; defaults to the locale's "Start date" / "Start month" / "Start year" wording. |
| displayFormat | string | follows picker | Display format (dayjs format), defaulting to YYYY-MM-DD / YYYY-MM / YYYY. It never changes the shape of the controlled value. |
| disabled | boolean | false | Disable |
| readOnly | boolean | false | Read only: can be opened for viewing, no endpoint selection/no preset/no clearing |
| className | string | - | Container class name |
Events
| Event | Type | Description |
|---|---|---|
| onValueChange | (range: [string, string] | null) => void | Interval changes (including clearing → null) |
DateRangePreset: { label: string; getValue: () => [string, string] }, called when clicked, can be dynamically calculated based on "today".
Usage guidelines
- Choose controlled or uncontrolled usage. Pair
valuewithonValueChange; usedefaultValueonly for an uncontrolled initial range, and do not pass both. - The external value is always an array of fixed-width strings, not Date objects, and its shape follows
picker:YYYY-MM-DD/YYYY-MM/YYYY.displayFormatchanges the trigger text only. - `minDate`, `maxDate`, and `disabledDate` always speak in ISO dates, regardless of
picker. At month and year granularity a cell is disabled only when the whole segment is out of bounds: withmaxDate="2026-06-15",2026-06is still selectable and2026-07is the first one greyed out. To exclude the current month as well, setmaxDateto the end of the previous segment (2026-05-31). - At month and year granularity
disabledDateis asked once per cell with the first day of that segment (2026-09-01stands for all of September). Do not put per-day logic such as "disable weekends" there; it has no meaning at those granularities. - The year page shows a full 12-year block, not a decade. With two pages side by side, a decade's leading and trailing filler years would make the same year appear on both pages.
disabledDatereceives an ISO date string. For weekday calculations, usenew Date(iso + "T00:00:00")and account for the local timezone. Avoidnew Date(iso), which parses as UTC and can shift the calendar day.- The trigger is a
role="combobox"button, and native attributes that are not listed in Props (aria-*,data-*,id,title,onBlur, …) land on it rather than on the outer container, which is the element that takes focus and that screen readers announce (#293). - Inside Field the label's
htmlFor,aria-describedby,invalid, anddisabledare wired to the trigger automatically, and so is thearia-requiredinjected by<Field required>. That chain was broken before 0.54.0 (the label pointed at an id that did not exist, so screen readers never announced the field name); upgrading needs no call-site change. - Query the trigger by role with
getByRole("combobox")in tests, not"button"anymore.
Related
Calendar · DatePicker · DateTimePicker · TimeField · Button · ShimmerButton
Playground
<DateRangePicker
value={range}
onValueChange={setRange}
/>