Time picker
A time of day typed as the crew say it or chosen from hour and minute lists, with its reference beside it.
On this page
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A night watch's start and end in UTC: each typed as the crew say it or chosen from the hour and minute lists with " +
"the arrows, Page Up and Page Down; the end cannot come before the start, and the lists say which clock Now reads.";
export default function Showcase(): ReactElement {
const [start, setStart]: [string, Dispatch<SetStateAction<string>>] = useState<string>("20:00");
const [end, setEnd]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
return (
<div
style={{
width: "100%",
maxWidth: 560,
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
alignContent: "start",
gap: "var(--space-lg)",
}}
>
<TimePicker
label="Watch starts"
required
timeReference="UTC"
now={Date.UTC(2026, 9, 23, 19, 42)}
value={start}
onValueChange={setStart}
/>
<TimePicker
label="Watch ends"
timeReference="UTC"
now={Date.UTC(2026, 9, 23, 19, 42)}
min={start === "" ? undefined : start}
value={end}
onValueChange={setEnd}
/>
</div>
);
}
A time picker is a text input for a time of day, with its reference after it and a clock button at the end. The
input reads a time as the crew type it, when they press Enter or leave the field: 14:30, 14.30, 14h30, 1430,
930, 14 for the hour, and on a 12-hour clock 2:30 pm or 2pm. It reads a time with its clock named after it,
as the log and messages write one: 14:05 UTC, 1405Z or 14:05 GMT on the UTC clock, 14:05 LT on the local
one, and an ISO instant from a log, whose offset turns it into the field's clock (2026-10-23T23:30:00-05:00 is
04:30 UTC). A time named on the other clock is refused with a line that says which clock the field takes. The page
receives HH:MM. The field holds a time of day only: a date and a time zone are the caller's, and timeReference
prints the reference ("UTC", "ship's time") beside the time, so a time is never shown without one.
The button opens two lists under the field, the hours and the minutes, every 5 minutes unless minuteStep says
otherwise. The keys drive them: the up and down arrows move an hour or a minute, Page Up and Page Down move six
hours or three steps, Home and End go to the ends, and typed digits go to that hour or minute. The right and left
arrows move between the lists. Enter on an hour moves to the minutes, Enter on a minute chooses the time and returns
to the button, and Escape or the Close button closes the lists and keeps the time the field had; while they are
open, Tab stays in them. Hours and minutes outside min and max are disabled. Under the lists, Now chooses the
current time, and a line says which clock it reads: UTC by default, as the log keeps times, or the device's with
zone="local".
With a finger the field is the browser's own time field (picker="auto", the default), whose wheels suit a touch
screen; with a mouse it is the kit's lists. picker="kit" or picker="native" chooses one everywhere. A typed time
the field cannot take keeps its text and names the problem: text it cannot read, or a time outside the bounds.
While it does, the field reports no time, so a form never saves the time that stood before the text.
Examples
Filled
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string = "A time already chosen, with its reference beside it.";
export default function Filled(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="High water" timeReference="UTC" defaultValue="06:30" />
</div>
);
}
Earliest and latest times
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string =
"With `min` and `max`, hours and minutes outside them are disabled in the lists, and a typed time past them is " +
"named: a lock that works from 06:30 to 18:00.";
export default function Bounds(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="Lock passage" timeReference="UTC" min="06:30" max="18:00" defaultValue="06:30" />
</div>
);
}
Minute step
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string =
"`minuteStep={15}` lists the quarter hours; a typed time still takes any minute, and its minute joins the list.";
export default function MinuteStep(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="Engine check" timeReference="UTC" minuteStep={15} defaultValue="09:10" />
</div>
);
}
Local time
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string =
"`zone=\"local\"` makes Now read the device's clock, for a time that belongs to someone ashore; the lists say so.";
export default function LocalZone(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="Taxi booked for" zone="local" timeReference="local time" />
</div>
);
}
Invalid
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string = "The caller's own error, shown in place of the field's.";
export default function Invalid(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker
label="Relief"
timeReference="UTC"
defaultValue="03:00"
error="The relief must come after the watch starts at 04:00."
/>
</div>
);
}
The browser's own field
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string =
"`picker=\"native\"` is the browser's own time field everywhere, as `auto` gives it under a finger.";
export default function Native(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="Arrival" timeReference="UTC" picker="native" defaultValue="12:04" />
</div>
);
}
Read-only
Code
import { TimePicker } from "@bosunkit/components/time-picker";
import type { ReactElement } from "react";
export const description: string = "A recorded time shown without letting the crew change it.";
export default function ReadOnly(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 280 }}>
<TimePicker label="Recorded at" timeReference="UTC" readOnly value="23:45" />
</div>
);
}
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 | string | undefined | – | The time as HH:MM, "" for none. |
| defaultValue | string | undefined | – | The time it starts with when the caller does not hold value. |
| onValueChange | ((value: string) => void) | undefined | – | Called with HH:MM when a time is chosen, or typed and the crew leave the field; "" when it is cleared. |
| min | string | undefined | – | The earliest time that can be chosen (HH:MM). |
| max | string | undefined | – | The latest time that can be chosen (HH:MM). |
| minuteStep | number | undefined | 5 | The minutes the list offers, every 5 unless set; a typed time can take any minute. |
| zone | ClockZone | undefined | "utc" | The clock that Now reads and the list states: utc by default, as the log keeps times; local for the browser's. |
| now | number | undefined | – | The current instant in epoch milliseconds, in place of the clock's, read on zone's clock. |
| timeReference | string | undefined | – | The time's reference, printed after it in the field, translated by the caller ("UTC", "ship's time"). |
| picker | TimePickerMode | undefined | "auto" | auto, the default, offers the kit's hour and minute lists with a mouse and the browser's own time wheels with a
finger (a coarse pointer); kit and native choose one everywhere. |
| hint | string | undefined | – | Text under the field, translated by the caller. |
| error | string | undefined | – | The caller's own error, shown in place of the field's. |
| required | boolean | undefined | – | Whether the field must hold a time. |
| disabled | boolean | undefined | – | Whether the field takes no input. |
| readOnly | boolean | undefined | – | Whether the field shows its time without letting the crew change it. |
| id | string | undefined | – | The text input's id, for a Label's htmlFor. |
| name | string | undefined | – | The form field name; the form receives HH:MM. |
| aria-label | string | undefined | – | Names the text input 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 text input. |
| motion | MotionProp | undefined | – | false opens the lists at once and keeps the error line still; true also moves the error line. |
Related
- Time input: the browser's time field in the kit's plain input frame.
- Date picker: a date chosen from a month calendar, built the same way.
- Duration input: a length of time rather than a time of day.
- Schema form: draws its
timefields with this picker.