Skip to content
design
Components

Date picker

A date typed or chosen from a month calendar that opens under the field and states its time zone.

On this page
14 October 2026
A passage's departure and arrival: UTC days chosen from a calendar or typed, the arrival bounded by the departure, each calendar stating its zone.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A passage's departure and arrival: the departure takes a UTC day from the current one to the end of the season, " +
  "typed or chosen from the calendar with arrows, Page Up and Page Down; the arrival cannot come before the " +
  "departure, and each calendar states the zone its days are in.";

export default function Showcase(): ReactElement {
  const [departure, setDeparture]: [string, Dispatch<SetStateAction<string>>] = useState<string>("2026-10-14");
  const [arrival, setArrival]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 560,
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
        alignContent: "start",
        gap: "var(--space-lg)",
      }}
    >
      <DatePicker
        label="Departure"
        required
        today="2026-10-11"
        min="2026-10-11"
        max="2026-11-30"
        value={departure}
        onValueChange={setDeparture}
      />
      <DatePicker
        label="Arrival"
        today="2026-10-11"
        min={departure === "" ? "2026-10-11" : departure}
        max="2026-12-31"
        value={arrival}
        onValueChange={setArrival}
      />
    </div>
  );
}

A date picker is a text input for a date, with a calendar button at the end. The input takes ISO 8601 (2026-10-11) or day, month and year with any separator, when the crew press Enter or leave it, and the field writes the chosen date out under it in the crew's language. The page receives ISO 8601.

The button opens a calendar under the field as a dialog, following the WAI-ARIA date picker dialog pattern: the month and year as a heading with buttons for the month before and after, and a grid with a column per weekday from the first day of the crew's language's week. The focused day is the one cell in the tab order. Arrow keys move a day or a week, Home and End go to the week's ends, Page Up and Page Down move a month, and with Shift a year; Enter or Space chooses the day and returns to the button, and Escape or the Close button closes the calendar. The open calendar is a modal dialog: Tab moves among its own controls and stays in it until it closes. The current day is marked, the chosen day is selected, and days outside min and max are disabled, with the keys stopping at them. Under the grid, two buttons choose the current day and the day before it in one press, each disabled when the bounds exclude its day.

Passages and the log keep dates in UTC, so the calendar's days are UTC days by default, and the calendar says so under the grid with the current day written out. zone="local" makes them the device's own days, for a date that belongs to a person ashore. A typed date the field cannot take keeps its text and names the problem: text it cannot read, a day the month lacks, or a date outside the bounds. While it does, the field reports no date, so a form never saves the date that stood before the text.

Examples

Empty

An empty date field; the button at the end opens the calendar on the current day, marked in the grid.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "An empty date field; the button at the end opens the calendar on the current day, marked in the grid.";

export default function Empty(): ReactElement {
  return <DatePicker label="Inspection due" today="2026-10-11" />;
}

Filled

1 March 2027
A held date in ISO 8601, written out under the field in the crew's language.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string = "A held date in ISO 8601, written out under the field in the crew's language.";

export default function Filled(): ReactElement {
  return <DatePicker label="Liferaft service due" today="2026-10-11" defaultValue="2027-03-01" />;
}

Earliest and latest dates

With `min` and `max`, days outside them are disabled in the grid, the keys stop at them, and a typed date past them is named.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "With `min` and `max`, days outside them are disabled in the grid, the keys stop at them, and a typed date past " +
  "them is named.";

export default function Bounds(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 360 }}>
      <DatePicker label="Haul-out" today="2026-10-11" min="2026-10-15" max="2026-11-15" />
    </div>
  );
}

Local days

A day kept in the device's time zone, such as a crew member's leave, says so in the calendar; passage and log dates keep the UTC default.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "A day kept in the device's time zone, such as a crew member's leave, says so in the calendar; passage and log " +
  "dates keep the UTC default.";

export default function LocalZone(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 360 }}>
      <DatePicker label="Leave starts" zone="local" today="2026-10-11" />
    </div>
  );
}

First day of the week

18 October 2026
The week starts on the crew's language's first day; `weekStartsOn` sets it, here Sunday.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "The week starts on the crew's language's first day; `weekStartsOn` sets it, here Sunday.";

export default function WeekStart(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 360 }}>
      <DatePicker label="Regatta" today="2026-10-11" weekStartsOn={0} defaultValue="2026-10-18" />
    </div>
  );
}

Written forms it reads

The page receives: no date yet
Type 2026-12-24, 24/12/2026, 24.12.2026 or 24 12 2026 and press Enter or leave the field: each becomes the ISO date the page receives.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Type 2026-12-24, 24/12/2026, 24.12.2026 or 24 12 2026 and press Enter or leave the field: each becomes the ISO date " +
  "the page receives.";

export default function WrittenForms(): ReactElement {
  const [date, setDate]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ width: "100%", maxWidth: 360, display: "grid", gap: "var(--space-sm)" }}>
      <DatePicker label="Christmas berth" today="2026-10-11" value={date} onValueChange={setDate} />
      <output style={{ fontSize: 12, color: "var(--text-dim)" }}>
        The page receives: {date === "" ? "no date yet" : date}
      </output>
    </div>
  );
}

Invalid

A typed date the field cannot take keeps its text and says why once the crew leave the field: a day the month lacks, a date past the bounds, or text it cannot read.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "A typed date the field cannot take keeps its text and says why once the crew leave the field: a day the month " +
  "lacks, a date past the bounds, or text it cannot read.";

export default function Invalid(): ReactElement {
  return (
    <DatePicker
      label="Departure"
      today="2026-10-11"
      defaultValue="2026-02-31"
      error="February 2026 has 28 days. Check the day."
    />
  );
}

Read-only

1 June 2026
A read-only date: the calendar button does not open.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string = "A read-only date: the calendar button does not open.";

export default function ReadOnly(): ReactElement {
  return <DatePicker label="Signed on" today="2026-10-11" defaultValue="2026-06-01" readOnly />;
}

Disabled

30 September 2026
A disabled date field.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string = "A disabled date field.";

export default function Disabled(): ReactElement {
  return <DatePicker label="Signed off" today="2026-10-11" defaultValue="2026-09-30" disabled />;
}

Inside a form field

15 April 2027
Inside a form field, named by `aria-label`.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import { FormField } from "@bosunkit/components/form-field";
import type { ReactElement } from "react";

export const description: string = "Inside a form field, named by `aria-label`.";

export default function InFormField(): ReactElement {
  return (
    <FormField label="Next antifouling">
      <DatePicker aria-label="Next antifouling" today="2026-10-11" defaultValue="2027-04-15" />
    </FormField>
  );
}

Paste

The paste button comes first at the end of the field, before the calendar button. It reads the same forms as the date of birth input's: ISO 8601, figures in a plain order or with dots, and months in words in any of the six languages. A date that could be read either way round is refused with a request for the day first, by the button and by Ctrl+V alike. The paste button shows only when the clipboard holds something the field can take, named by what it would paste (Paste 24 December 2026). To know that, the field reads the clipboard when it takes focus and when the window comes back, and only where the browser lets the page read it without asking: the clipboard-read permission is granted. Where the browser would ask first, or does not know the permission (Firefox, Safari), the field reads nothing and shows no button, so no prompt appears on focus; Ctrl+V and a long press on a phone paste through the same reader. A read-only or disabled field shows no paste button.

  • Departure 24 December 2026, 08:00 UTC
  • 2026-12-24T08:00:00Z
  • 24 décembre 2026
  • 12/24/2026
  • 03/04/2026
Copy one of these lines, then focus the field: where the browser lets the page read the clipboard, a paste button named by the date appears before the calendar button; Ctrl+V works everywhere. The last line is refused, as its day and month could be either way round.
Code
import { DatePicker } from "@bosunkit/components/date-picker";
import type { ReactElement } from "react";

export const description: string =
  "Copy one of these lines, then focus the field: where the browser lets the page read the clipboard, a paste button named by the date appears before the calendar button; Ctrl+V works everywhere. The last line is refused, as its day and month could be either way round.";

const SAMPLES: readonly string[] = [
  "Departure 24 December 2026, 08:00 UTC",
  "2026-12-24T08:00:00Z",
  "24 décembre 2026",
  "12/24/2026",
  "03/04/2026",
];

export default function Paste(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 520, display: "grid", gap: "var(--space-md)" }}>
      <DatePicker label="Departure" today="2026-10-11" />
      <ul style={{ margin: 0, paddingLeft: "var(--space-lg)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
        {SAMPLES.map((sample: string): ReactElement => (
          <li key={sample} style={{ userSelect: "all", overflowWrap: "anywhere" }}>
            {sample}
          </li>
        ))}
      </ul>
    </div>
  );
}

Props

PropTypeDefaultDescription
labelstring | undefined–The field's name on the outline, translated by the caller; omit it inside a FormField.
valuestring | undefined–The date as ISO 8601 (2026-10-11), "" for none.
defaultValuestring | undefined–The date it starts with when the caller does not hold value.
onValueChange((value: string) => void) | undefined–Called with the ISO date when one is chosen or typed and the crew leave the field; "" when it is cleared.
minstring | undefined–The earliest date that can be chosen (ISO 8601).
maxstring | undefined–The latest date that can be chosen (ISO 8601).
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; the chosen date written out when unset.
writeDateboolean | undefinedtrueWhether the chosen date is written out under the field when there is no hint; on by default. Off for a field in a group that writes the date out itself, such as the date and time input.
errorstring | undefined–The caller's own error, shown in place of the field's.
requiredboolean | undefined–Whether the field must hold a date.
disabledboolean | undefined–Whether the field takes no input.
readOnlyboolean | undefined–Whether the field shows its date without letting the crew change it.
idstring | undefined–The text input's id, for a Label's htmlFor.
namestring | undefined–The form field name; the form receives the ISO date.
aria-labelstring | undefined–Names the text input 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 text input.
motionMotionProp | undefined–false opens the calendar at once and keeps the error line still; true also moves the error line.