Skip to content
cennge-ui
GitHubv0.3.1, changelog

TimeInput

A time of day on the locale's clock: type it the short way, or move the hours and minutes with the arrow keys.

Between 9 AM and 6 PM
On a 24-hour clock

Both values are "HH:mm": "14:30" and "02:30"

  1. Up and Down change the part under the caret: the hour, the minutes by the step (15 in the release call), or AM and PM. The part stays selected.
  2. Type 3p, 14.30 or 1145 and press Tab: the field shows it on its clock, and the value stays HH:mm.
  3. Type 7 pm in the release call: it is after 6 PM, so the text stays with the reason; Escape puts the time back.
Live: the component itself, not a picture of it.

Field marks

Import
import { TimeInput } from "cennge-ui";
Size
4.9 KB gzip for this import, its dependencies aside
Pattern
Text input; on a 24-hour clock phones show the number pad
Keyboard
Arrow up Arrow down Enter Esc
State
Controlled with value and onValueChange ("HH:mm" or null), or uncontrolled
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Clock
12 or 24 hours from the locale, or from hourCycle
Form value
"HH:mm" in a hidden input under name
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: time-input

When to use#

Use a TimeInput for a time of day: when a call starts, when a nightly job runs, when a release goes out. The field shows the time the way the locale writes it, with AM and PM or without, and the value is always "HH:mm" on a 24-hour clock, like <input type="time">.

Typing is forgiving: 14:30, 14.30, 1430, 930, 9, 2:30 pm, 2p. The text is read when the field loses focus or on Enter; without AM or PM the hour counts on a 24-hour clock.

Similar#

  • DatePicker takes the date; with a TimeInput next to it you have a date and a time, as in the example below.
  • NumberInput is for an amount of time, such as a timeout in minutes, rather than a time of day.

Type it or step it#

Arrow keys work on the part under the caret: the hours move by one, the minutes by step, and on a 12-hour clock the AM or PM switches the half of the day. They stop at minValue and maxValue. A typed time is kept to the minute, not rounded to the step. In an empty field they count from minValue, or from the current time. Emptying the field sets the value to null.

value: null

basic.tsx
import { useState } from "react";
import { TimeInput } from "cennge-ui";

export default function DeployTime() {
  const [time, setTime] = useState<string | null>(null);

  return (
    <div className="flex w-full max-w-56 flex-col gap-3">
      <TimeInput label="Deploy at" value={time} onValueChange={setTime} />
      <p className="text-sm text-muted">
        value: <code className="font-code text-foreground">{time ? `"${time}"` : "null"}</code>
      </p>
    </div>
  );
}

12 or 24 hours#

The clock comes from Intl for the locale: 12 hours in the United States, 24 in Britain. hourCycle overrides it. These three share one value: change it in any of them.

clocks.tsx
import { useState } from "react";
import { TimeInput } from "cennge-ui";

export default function Clocks() {
  const [time, setTime] = useState<string | null>("21:45");

  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      <TimeInput label="en-US" locale="en-US" value={time} onValueChange={setTime} />
      <TimeInput label="en-GB" locale="en-GB" value={time} onValueChange={setTime} />
      <TimeInput label="en-US, 24 hours" locale="en-US" hourCycle={24} value={time} onValueChange={setTime} />
    </div>
  );
}

A date and a time#

Name both fields and the form sends "2026-11-19" and "10:00"; join them as `${date}T${time}`. The result is a local date and time with no time zone, which is what a person picks; turn it into an instant where you know whose time zone it is.

date-and-time.tsx
import { useState } from "react";
import { Button, DatePicker, Form, TimeInput } from "cennge-ui";

export default function ScheduleRelease() {
  const [scheduled, setScheduled] = useState<string | null>(null);

  return (
    <Form
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        // A local date and time, no time zone: add the offset where you know it.
        setScheduled(`${data.get("date")}T${data.get("time")}`);
      }}
      className="flex w-full max-w-96 flex-col gap-4"
    >
      <div className="grid grid-cols-[minmax(0,1fr)_8.5rem] gap-3">
        <DatePicker name="date" label="Publish on" defaultValue="2026-11-19" isRequired />
        <TimeInput name="time" label="At" defaultValue="10:00" step={15} isRequired />
      </div>
      <div className="flex flex-wrap items-center gap-3">
        <Button type="submit" color="primary">
          Schedule payload-cache
        </Button>
        {scheduled ? <code className="font-code text-sm text-muted">{scheduled}</code> : null}
      </div>
    </Form>
  );
}

In another language#

formatError receives invalid with the pattern, or outOfRange with the limits as "HH:mm". The clock, the AM and PM words it reads and shows, and the format follow the locale of CenngeProvider or of the field.

translated.tsx
import { CenngeProvider, TimeInput, type TimeInputError } from "cennge-ui";

const errorText = (error: TimeInputError) =>
  error.type === "invalid" ? "Uhrzeit als HH:MM eingeben" : `Nur zwischen ${error.min} und ${error.max} Uhr`;

export default function GermanTime() {
  return (
    // de-DE writes times on a 24-hour clock, so the field does too.
    <CenngeProvider locale="de-DE">
      <TimeInput
        label="Wartung ab"
        minValue="22:00"
        maxValue="23:45"
        step={15}
        formatError={errorText}
        className="w-full max-w-56"
      />
    </CenngeProvider>
  );
}

Accessibility#

The field is a text input with its label. The arrow keys change its text without typing, so a polite live region reads the new time after each step. A refusal sets aria-invalid and describes the field with the reason. On a 24-hour clock the field asks phones for the number pad; with AM and PM it keeps the letters.

Props#

PropTypeDefault
label
React.ReactNode
–
description
React.ReactNode
–
errorMessage
React.ReactNodeShown instead of description while isInvalid.
–
value
string | null"HH:mm" on a 24-hour clock, e.g. "14:30"; null when empty.
–
defaultValue
string | null
–
onValueChange
(value: string | null) => void
–
minValue
stringEarliest time, "HH:mm".
–
maxValue
stringLatest time, "HH:mm".
–
step
numberMinutes the arrow keys move in the minutes part; times snap to it.
1
hourCycle
122412 shows AM/PM.
the locale's clock
locale
string
the provider's locale, else "en-US"
formatError
(error: TimeInputError) => stringText under the field when typed text is refused. Override to translate.
–
isInvalid
boolean
–
isRequired
boolean
–
isDisabled
boolean
–
isReadOnly
boolean
–
variant
"bordered""flat""faded""underlined"
"flat"
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
radius
"none""sm""md""lg""full"
"md"
startContent
React.ReactNode
–
endContent
React.ReactNode
–
classNames
Partial<Record<TimeInputSlots, string>>
–

Also takes every attribute of <input>, except size, color, value, defaultValue, onChange, min, max, step, type, which mean something else here.