Rxova
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
09Type 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:0006: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.