Skip to content
design
Components

Date of birth input

A date of birth as day, month and year segments, each typed or chosen from a grid.

On this page
Date of birth
As on the passport.
The page receives: no date yet
A crew member's date of birth: segments that open a grid of days, months or years, take typed digits and a pasted date, and refuse a day the month lacks or a date after the current day.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A crew member's date of birth, required: press a segment for its grid (days the month lacks are disabled, months " +
  "by name, years back 120), or type the digits and watch the focus move on; paste 02/03/1984, or try 31 February " +
  "or a day after 11 October 2026 to see the field refuse it.";

export default function Showcase(): ReactElement {
  const [born, setBorn]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ display: "grid", justifyItems: "start", gap: "var(--space-sm)" }}>
      <DateOfBirthInput
        label="Date of birth"
        required
        today="2026-10-11"
        value={born}
        onValueChange={setBorn}
        hint="As on the passport."
      />
      <output style={{ fontSize: 12, color: "var(--text-dim)" }}>
        The page receives: {born === "" ? "no date yet" : born}
      </output>
    </div>
  );
}

A date of birth input is one outlined field with three segments, DD, MM and YYYY, in the order every one of the crew's languages writes a date. Each segment is a button with a chevron, named by its part and value (Month: March). Pressing it opens a panel under the field, titled with the part, holding a grid of the choices it can take: the days in six columns, with the days the chosen month and year lack disabled; the months by name; the years from the current one back 120 years, or back to min. Arrow keys move by a cell or a row, Home and End go to the ends, Enter or Space chooses and moves to the next segment, and Escape closes the panel. The chevron points up while its panel is open.

Digits typed into a focused segment fill it and move on: a day of 4 or more and a month of 2 or more need no second digit. Two digits make a year already, the latest ending in them that minAge allows: 93 is 1993, and 20 is 2020, or 1920 with minAge 18; typing on to four digits makes any year. Letters on the month type its name, and in an open grid, typing a name or a number jumps to the matching choice. Backspace clears a segment, and on an empty one the one before it. A pasted date, in ISO 8601 or as day, month and year with any separator, fills all three.

The page receives the date in ISO 8601, or "" until it is whole and valid. A day the calendar does not have, such as 31 February, and a date after the current day are refused as soon as the date is whole; a missing part is named once the crew leave the field.

Age bounds

Date of birth
A crew member at least 16 and at most 65: the bounds take no date outside the ages, and say the age, not the date.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";
import { useState, type Dispatch, type SetStateAction } from "react";

export const description: string =
  "A crew member at least 16 and at most 65: the bounds take no date outside the ages, and say the age, not the date.";

export default function Age(): ReactElement {
  const [value, setValue]: [string, Dispatch<SetStateAction<string>>] = useState<string>("1995-06-14");
  return (
    <DateOfBirthInput
      label="Date of birth"
      required
      today="2026-10-11"
      minAge={16}
      maxAge={65}
      value={value}
      onValueChange={setValue}
    />
  );
}

minAge and maxAge bound the date by the crew member's age rather than by dates on a calendar: minAge 16 takes no birth date later than 16 years before the current day, maxAge 65 none earlier than 65 years. The year grid runs within the bounds, and the message says the age — "at least 16", "at most 65" — not a date to count from.

Examples

Empty

Date of birth
Three empty segments, DD, MM and YYYY, in the order every one of the crew's languages writes a date.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";

export const description: string =
  "Three empty segments, DD, MM and YYYY, in the order every one of the crew's languages writes a date.";

export default function Empty(): ReactElement {
  return <DateOfBirthInput label="Date of birth" today="2026-10-11" />;
}

Filled

Date of birth
A held date: each segment is named with its value, the month by name.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";

export const description: string = "A held date: each segment is named with its value, the month by name.";

export default function Filled(): ReactElement {
  return <DateOfBirthInput label="Date of birth" today="2026-10-11" defaultValue="1979-05-14" />;
}

Written forms it reads

Date of birth
Press a form to paste it into the day segment as the clipboard would: ISO 8601, or day, month and year with any separator.
Code
import { Button } from "@bosunkit/components/button";
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import { useRef, useState, type Dispatch, type ReactElement, type RefObject, type SetStateAction } from "react";

export const description: string =
  "Press a form to paste it into the day segment as the clipboard would: ISO 8601, or day, month and year with any " +
  "separator.";

const FORMS: readonly string[] = ["1984-03-02", "02/03/1984", "2.3.1984", "2 3 1984"];

export default function WrittenForms(): ReactElement {
  const [born, setBorn]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  const field: RefObject<HTMLDivElement | null> = useRef<HTMLDivElement | null>(null);

  function paste(text: string): void {
    const data: DataTransfer = new DataTransfer();
    data.setData("text/plain", text);
    field.current
      ?.querySelector("button")
      ?.dispatchEvent(new ClipboardEvent("paste", { clipboardData: data, bubbles: true }));
  }

  return (
    <div style={{ width: "100%", maxWidth: 360, display: "grid", gap: "var(--space-sm)" }}>
      <DateOfBirthInput ref={field} label="Date of birth" today="2026-10-11" value={born} onValueChange={setBorn} />
      <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-xs)" }}>
        {FORMS.map((form: string): ReactElement => (
          <Button
            key={form}
            size="sm"
            variant="ghost"
            onClick={(): void => {
              setBorn("");
              paste(form);
            }}
          >
            {form}
          </Button>
        ))}
      </div>
    </div>
  );
}

Invalid

Date of birth
A whole date the field cannot take is refused at once: here a birth date after the current day. Type 31 02 1984 to see a day the month lacks refused with the month's length.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";

export const description: string =
  "A whole date the field cannot take is refused at once: here a birth date after the current day. Type 31 02 1984 " +
  "to see a day the month lacks refused with the month's length.";

export default function Invalid(): ReactElement {
  return <DateOfBirthInput label="Date of birth" today="2026-10-11" defaultValue="2027-01-05" />;
}

Read-only

Skipper's date of birth
A read-only date opens no grid and takes no digits.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";

export const description: string = "A read-only date opens no grid and takes no digits.";

export default function ReadOnly(): ReactElement {
  return <DateOfBirthInput label="Skipper's date of birth" today="2026-10-11" defaultValue="1979-05-14" readOnly />;
}

Disabled

Date of birth
A disabled date of birth.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
import type { ReactElement } from "react";

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

export default function Disabled(): ReactElement {
  return <DateOfBirthInput label="Date of birth" today="2026-10-11" defaultValue="1988-11-02" disabled />;
}

Inside a form field

Inside a form field, the group named by `aria-label`.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
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="Guest's date of birth">
      <DateOfBirthInput aria-label="Guest's date of birth" today="2026-10-11" />
    </FormField>
  );
}

Paste

The paste button, at the end of the field, fills all three segments. It reads:

  • ISO 8601, with or without a time after it;
  • day, month and year in figures, with the day first when the figures use dots (14.07.2002);
  • figures with slashes, hyphens or spaces when their order is plain: a first number past 12 is the day, and a second past 12 makes it a month-first date (07/14/2002);
  • a date with its month in words in any of the six languages, in either order, with ordinals and three-letter abbreviations (14 July 2002, 14. Juli 2002, 1er mars 2002, July 14, 2002).

Two different numbers of 12 or less, such as 07/05/2002, could be either date, and the field asks for the day first with the month in words. The paste button shows only when the clipboard holds something the field can take, named by what it would paste (Paste 14 July 2002). 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.

Date of birth
  • 14 July 2002
  • 14. Juli 2002
  • 14 de julio de 2002
  • 07/14/2002
  • 14.07.2002
  • 07/05/2002
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; Ctrl+V on a segment works everywhere. Months in words in any of the six languages and figures in a plain order fill the date; the last line, which could be 7 May or 5 July, is refused with a request for the day first.
Code
import { DateOfBirthInput } from "@bosunkit/components/date-of-birth-input";
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; Ctrl+V on a segment works everywhere. Months in words in any of the six languages and figures in a plain order fill the date; the last line, which could be 7 May or 5 July, is refused with a request for the day first.";

const SAMPLES: readonly string[] = [
  "14 July 2002",
  "14. Juli 2002",
  "14 de julio de 2002",
  "07/14/2002",
  "14.07.2002",
  "07/05/2002",
];

export default function Paste(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 520, display: "grid", gap: "var(--space-md)" }}>
      <DateOfBirthInput label="Date of birth" 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 (1984-03-02), "" while the field does not hold a whole, valid one.
defaultValuestring | undefined–The date it starts with when the caller does not hold value.
onValueChange((value: string) => void) | undefined–Called with the ISO date once day, month and year make a valid one, and with "" when they no longer do.
minAgenumber | undefined–The youngest the person may be, in whole years: no birth date later than that many years before today.
maxAgenumber | undefined–The oldest the person may be, in whole years: no birth date earlier than that many years before today.
todaystring | undefined–Today's date (ISO 8601): no birth date after it is taken. The browser's local date when unset.
minstring | undefined–The earliest date taken (ISO 8601); 120 years before today when unset.
hintstring | undefined–Text under the field, translated by the caller.
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 day segment's id, for a Label's htmlFor.
namestring | undefined–The form field name; the form receives the 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<HTMLDivElement> | undefined–The field's root element.
motionMotionProp | undefined–false opens the choice panels at once and keeps the error line still; true also moves the error line.