Date range picker
A range of days typed as its first and last day or chosen in two presses, within dates and a number of days the caller allows.
On this page
Code
import { DateRangePicker, type DateRange } from "@bosunkit/components/date-range-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"Two ranges with limits: a log export of at most 31 UTC days that cannot end after the current one, and a charter " +
"of at least two days inside the season. The calendar takes the first day and then the last, and offers only last " +
"days the limits allow.";
export default function Showcase(): ReactElement {
const [logExport, setExport]: [DateRange, Dispatch<SetStateAction<DateRange>>] = useState<DateRange>({
start: "2026-10-01",
end: "2026-10-11",
});
const [charter, setCharter]: [DateRange, Dispatch<SetStateAction<DateRange>>] = useState<DateRange>({
start: "",
end: "",
});
return (
<div style={{ width: "100%", display: "flex", flexWrap: "wrap", gap: "var(--space-lg)" }}>
<DateRangePicker
label="Log export"
today="2026-10-11"
max="2026-10-11"
maxDays={31}
value={logExport}
onValueChange={setExport}
/>
<DateRangePicker
label="Charter"
today="2026-10-11"
min="2026-10-11"
max="2026-11-30"
minDays={2}
maxDays={14}
value={charter}
onValueChange={setCharter}
/>
</div>
);
}
A date range picker holds a range of whole days, both ends included: a first day and a last day, each a text input
that takes ISO 8601 (2026-10-11) or day, month and year with any separator, either side of a dash, with a calendar
button at the end. Moving from the first day to the last is not leaving the field; when the crew leave it, or press
Enter, a range within every limit is reported, the inputs take ISO 8601, and the range is written out under the
field with the number of days it holds. The page receives both ends in ISO 8601, and only for a whole range within
its limits, or both empty when the crew clear it.
The calendar is the date picker's, with its keys, its marked current day and its zone note, and it takes a range in two presses. The line above the grid says which end comes next. The first press sets the first day; the second sets the last day and closes the calendar. A day before the first starts the range again from that day. A whole range opened again is chosen afresh from its first day. Every day of the range is selected, its ends in the accent and the days between in a tint of it.
The limits are the dates either end can fall on, min and max, and the number of days the range can hold,
minDays and maxDays, both ends counted, so minDays={2} asks for at least one night aboard. Once the first day
is chosen, the calendar disables every last day that would break a limit, and the limits are written under the
grid. A typed range that breaks one keeps its text and names the problem and the day that would fix it: "A range
can be at most 31 days. Choose a last day up to 31 October 2026." While it does, and while only one day of a range is
chosen, the field reports no range, so a form never saves the range that stood before.
Examples
Empty
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string = "Empty: a first and a last day to type, and the calendar button at the end.";
export default function Empty(): ReactElement {
return <DateRangePicker label="Passage window" today="2026-10-11" />;
}
Filled
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string = "Filled: the range written out under the field with the number of days it holds.";
export default function Filled(): ReactElement {
return (
<DateRangePicker label="Passage window" today="2026-10-11" defaultValue={{ start: "2026-10-14", end: "2026-10-20" }} />
);
}
Fewest and most days
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string =
"With `minDays` and `maxDays`, both ends counted: once the first day is chosen, the calendar disables every last " +
"day that would make the range too short or too long, and states the limits under the grid.";
export default function SpanLimits(): ReactElement {
return <DateRangePicker label="Crew rotation" today="2026-10-11" minDays={7} maxDays={28} />;
}
Earliest and latest dates
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string =
"With `min` and `max`, neither end can fall outside them: their days are disabled in the grid and a typed day past " +
"them is named.";
export default function Bounds(): ReactElement {
return <DateRangePicker label="Yard period" today="2026-10-11" min="2026-11-01" max="2027-03-31" />;
}
Invalid
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string =
"A range the field cannot take keeps its text and says why once the crew leave the field: a last day before the " +
"first, more or fewer days than the limits allow, a missing end, or a day outside the bounds.";
export default function Invalid(): ReactElement {
return (
<DateRangePicker
label="Log export"
today="2026-10-11"
maxDays={31}
defaultValue={{ start: "2026-08-01", end: "2026-10-11" }}
error="A range can be at most 31 days. Choose a last day up to 31 August 2026."
/>
);
}
Read-only
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string = "Read-only: the range shown with a dashed outline, and no calendar.";
export default function ReadOnly(): ReactElement {
return (
<DateRangePicker label="Insured period" today="2026-10-11" readOnly defaultValue={{ start: "2026-04-01", end: "2027-03-31" }} />
);
}
Disabled
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import type { ReactElement } from "react";
export const description: string = "Disabled: dimmed, and takes no input.";
export default function Disabled(): ReactElement {
return <DateRangePicker label="Passage window" today="2026-10-11" disabled />;
}
Inside a form field
Code
import { DateRangePicker } from "@bosunkit/components/date-range-picker";
import { FormField } from "@bosunkit/components/form-field";
import type { ReactElement } from "react";
export const description: string = "Inside a form field, the group named by `aria-label`.";
export default function InFormField(): ReactElement {
return (
<FormField label="Engine log period">
<DateRangePicker aria-label="Engine log period" today="2026-10-11" maxDays={92} />
</FormField>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | undefined | – | The field's name on the outline, translated by the caller; omit it inside a FormField. |
| value | DateRange | undefined | – | The range, both ends ISO 8601 dates. |
| defaultValue | DateRange | undefined | – | The range it starts with when the caller does not hold value. |
| onValueChange | ((value: DateRange) => void) | undefined | – | Called with the range when the crew finish one that is within every limit, both ends chosen or typed and the field
left; called with both ends "" when they clear it. A half-chosen or out-of-limit range is not reported. |
| min | string | undefined | – | The earliest day either end can be (ISO 8601). |
| max | string | undefined | – | The latest day either end can be (ISO 8601). |
| minDays | number | undefined | – | The fewest days a range can hold, both ends counted: 2 asks for at least one night. |
| maxDays | number | undefined | – | The most days a range can hold, both ends counted: 31 for a month of the log. |
| zone | ClockZone | undefined | "utc" | The clock that decides today's date, which the calendar marks: utc by default, as passages and the log keep
dates; local for the browser's time zone. The calendar says which. |
| today | string | undefined | – | Today's date (ISO 8601), in place of the clock's. |
| weekStartsOn | number | undefined | – | The first day of the week, 0 for Sunday to 6 for Saturday; the crew's language's custom when unset. |
| hint | string | undefined | – | Text under the field, translated by the caller. When unset, the line says which day to choose next until both are there, then writes the range out with its days. |
| error | string | undefined | – | The caller's own error, shown in place of the field's. |
| required | boolean | undefined | – | Whether the field must hold a range. |
| disabled | boolean | undefined | – | Whether the field takes no input. |
| readOnly | boolean | undefined | – | Whether the field shows its range without letting the crew change it. |
| id | string | undefined | – | The first day's input id, for a Label's htmlFor. |
| startName | string | undefined | – | The form field name of the first day; the form receives its ISO date. |
| endName | string | undefined | – | The form field name of the last day; the form receives its ISO date. |
| aria-label | string | undefined | – | Names the field when there is no label. |
| className | string | undefined | – | Classes added to the field's root. |
| testId | string | undefined | – | data-testid on the field. |
| ref | Ref<HTMLInputElement> | undefined | – | The first day's text input. |
| motion | MotionProp | undefined | – | false opens the calendar at once and keeps the error line still; true also moves the error line. |
Related
- Date picker: one day, from the same calendar.
- Duration input: a length of time rather than the days it falls on.
- Outlined field: the box every maritime input draws in.