Skip to content
design
Components

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
Log export
1 October 2026 to 11 October 2026, 11 days
Charter
Choose the first day.
A log export of at most 31 UTC days that ends no later than the current one, and a charter of two to fourteen days inside the season.
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

Passage window
Choose the first day.
Empty: a first and a last day to type, and the calendar button at the end.
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

Passage window
14 October 2026 to 20 October 2026, 7 days
Filled: the range written out under the field with the number of days it holds.
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

Crew rotation
Choose the first day.
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.
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

Yard period
Choose the first day.
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.
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

Log export
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.
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

Insured period
1 April 2026 to 31 March 2027, 365 days
Read-only: the range shown with a dashed outline, and no calendar.
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

Passage window
Disabled: dimmed, and takes no input.
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

Choose the first day.
Inside a form field, the group named by `aria-label`.
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

PropTypeDefaultDescription
labelstring | undefined–The field's name on the outline, translated by the caller; omit it inside a FormField.
valueDateRange | undefined–The range, both ends ISO 8601 dates.
defaultValueDateRange | 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.
minstring | undefined–The earliest day either end can be (ISO 8601).
maxstring | undefined–The latest day either end can be (ISO 8601).
minDaysnumber | undefined–The fewest days a range can hold, both ends counted: 2 asks for at least one night.
maxDaysnumber | undefined–The most days a range can hold, both ends counted: 31 for a month of the log.
zoneClockZone | 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.
todaystring | undefined–Today's date (ISO 8601), in place of the clock's.
weekStartsOnnumber | undefined–The first day of the week, 0 for Sunday to 6 for Saturday; the crew's language's custom when unset.
hintstring | 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.
errorstring | undefined–The caller's own error, shown in place of the field's.
requiredboolean | undefined–Whether the field must hold a range.
disabledboolean | undefined–Whether the field takes no input.
readOnlyboolean | undefined–Whether the field shows its range without letting the crew change it.
idstring | undefined–The first day's input id, for a Label's htmlFor.
startNamestring | undefined–The form field name of the first day; the form receives its ISO date.
endNamestring | undefined–The form field name of the last day; the form receives its ISO date.
aria-labelstring | undefined–Names the field when there is no label.
classNamestring | undefined–Classes added to the field's root.
testIdstring | undefined–data-testid on the field.
refRef<HTMLInputElement> | undefined–The first day's text input.
motionMotionProp | undefined–false opens the calendar at once and keeps the error line still; true also moves the error line.