Skip to content

Time input — Usage

Type digits — the field advances by itself. a and p set the day period, arrows step, Backspace clears. (TimeInput and useState are already in scope.)

Editable — try changing it
function TryIt() {
  const [value, setValue] = useState(null);
  return (
    <div style={{ display: "grid", gap: "0.75rem" }}>
      <TimeInput label="Start time" locale="en-US" value={value} onChange={setValue} />
      <code>{value === null ? "null" : value}</code>
    </div>
  );
}

Note the value stays 24-hour while the field shows AM/PM. Try 12 then a — midnight is 00:00, not 12:00.

import { useState } from "react";
import { TimeInput } from "@rxova/react-time-input";
function Controlled() {
const [value, setValue] = useState<string | null>("14:30");
return <TimeInput label="At" value={value} onChange={setValue} />;
}
function Uncontrolled() {
return <TimeInput label="At" defaultValue="14:30" />;
}

onChange fires when the time becomes complete and valid, and when it stops being — never with a half-typed number in between. onPartsChange is the per-keystroke channel if you want one.

Editable — try changing it
function Clocks() {
  return (
    <div style={{ display: "grid", gap: "0.5rem" }}>
      <TimeInput label="United States" locale="en-US" defaultValue="14:05" />
      <TimeInput label="United Kingdom" locale="en-GB" defaultValue="14:05" />
      <TimeInput label="Japan (forced 12-hour)" locale="ja-JP" hour12 defaultValue="14:05" />
    </div>
  );
}

Omit locale to use the runtime’s own; pass hour12 to force either clock.

Editable — try changing it
function Precise() {
  return (
    <TimeInput
      label="Precise time"
      locale="en-GB"
      showSeconds
      minuteStep={15}
      secondStep={30}
      defaultValue="09:00:00"
    />
  );
}

Steps must divide 60. A 7-minute step leaves a 4-minute bucket at the top of every hour, so it is refused and reported through onWarn. Steps apply to arrow-key stepping, not to typing — a user can still type 09:07 under a 15-minute step, because enforcing the grid mid-entry fights them.

KeyEffect
0–9Type into the focused segment; advances when no further digit could fit
a / pSet AM or PM (also the localised first letter)
↑ / ↓Step the focused segment, wrapping at its ends
← / →Move between segments; stops at the ends rather than cycling
Home / EndJump the focused segment to its minimum or maximum
Backspace / DeleteClear the focused segment
TabMove to the next segment, then out of the field
import { TimeInput } from "@rxova/react-time-input";
function Opening() {
return <TimeInput label="Appointment" min="09:00" max="17:00" />;
}

Both bounds are inclusive. A completed time outside the range is still reported, with the field marked data-out-of-range and aria-invalid. Pass emitOutOfRange={false} to get null instead.

A min after the max is dropped and reported: 22:00–06:00 is two ranges, and this component does not model a range that wraps past midnight.

import { TimeInput } from "@rxova/react-time-input";
function Form() {
return (
<form action="/bookings" method="post">
<TimeInput label="Starts at" name="at" locale="en-US" defaultValue="14:30" />
<button type="submit">Save</button>
</form>
);
}

The box shows 02:30 PM; the form receives 14:30. required becomes aria-required on the group — a hidden input is barred from constraint validation — so enforcing it stays with your form layer.

No stylesheet to import.

[data-rx-time-root] {
--rx-time-gap: 0.125rem;
--rx-time-literal-opacity: 0.5;
}
[data-rx-time-segment][data-focused] {
background: Highlight;
color: HighlightText;
}
[data-rx-time-segment][data-placeholder] {
opacity: 0.55;
}

The data-* attributes are public API, covered by semver. The full list is in the package README.

useTimeInput gives you the whole state machine with no markup — including the 12/24-hour translation and the digit buffer.

import { useTimeInput } from "@rxova/react-time-input";
function CustomField() {
const field = useTimeInput({ locale: "en-US" });
return (
<div onBlur={field.handleBlur}>
{field.pieces.map((piece, index) =>
piece.kind === "literal" ? (
<span key={index}>{piece.text}</span>
) : (
<span
key={piece.type}
ref={(node) => {
field.segmentRefs.current[piece.type] = node;
}}
role="spinbutton"
tabIndex={0}
onFocus={(event) => {
field.handleSegmentFocus(piece.type, event);
}}
onKeyDown={(event) => {
if (/^\d$/.test(event.key)) field.typeDigit(piece.type, event.key);
else if (event.key === "ArrowUp") field.step(piece.type, 1);
else if (event.key === "ArrowDown") field.step(piece.type, -1);
else field.typeLetter(piece.type, event.key);
}}
>
{field.displayValue(piece.type) ?? "--"}
</span>
),
)}
</div>
);
}

field.clear() empties every segment at once — not bound to a key in the default renderer, where Backspace clears one segment, but available for a custom one.

The clock helpers are exported too — toISO, fromISO, toDisplayHour, fromDisplayHour, toDayPeriod, compareISO, withinRange — all pure and all Date-free.