|Kit

Time Input

HOUR/MINUTE (+ SECOND) + AM/PM time entry composed from NumberInput + SegmentGroup — Ark ships no time-picker machine, so this is the kit's own composed time field.

TimeInput is a standalone time entry — HOUR / MINUTE (+ SECOND) steppers next to an AM/PM toggle, composed from NumberInput and SegmentGroup. Ark UI has no time-picker machine (only a countdown timer), so this is hand-composed from the kit's own primitives rather than a wrapped Ark part — it's the same time row DatePicker will embed in its popover once it lands.

import { TimeInput } from '@42-central/ui-react/time-input';

<TimeInput label="Meeting time" defaultValue={{ hour: 9, minute: 30 }} />

The value is { hour, minute, second? } | null — hour is always stored in 24h, regardless of what's displayed. There's no built-in range mode: a "start" / "end" pair reads better as two labelled TimeInputs than as one control split down the middle with nothing to tell its two halves apart — see Start / end pairs.

Playground

Component

Loading preview

Props

Code

<Component
  label="Meeting time"
  minuteStep={1}
  placeholder="--"
/>

12h vs 24h

Display defaults to the active locale's own convention (Intl.DateTimeFormat's hourCycle) — the AM/PM segment group only renders in a 12h locale. Override it with format (12 | 24) or hourCycle for the exact Unicode cycle ("h11" | "h12" | "h23" | "h24"). The stored hour is always 24h either way — only the display and the AM/PM segment change.

// Force 24h regardless of locale
<TimeInput format={24} defaultValue={{ hour: 14, minute: 5 }} />

// Force 12h regardless of locale
<TimeInput format={12} defaultValue={{ hour: 14, minute: 5 }} />

Start / end pairs

There's no range prop. Two TimeInputs, each with its own label, reads more clearly than one control with an unlabelled start half and end half — compose them yourself:

Start time
Period
End time
Period

Code

<div className="flex gap-4">
  <TimeInput label="Start time" defaultValue={{ hour: 9, minute: 0 }} />
  <TimeInput label="End time" defaultValue={{ hour: 17, minute: 30 }} />
</div>

Seconds & minute step

withSeconds adds a third stepper; minuteStep sets the minute increment (e.g. 15 for quarter-hour scheduling). Every stepper wraps at its bounds (23 → 0, 59 → 0, …) instead of clamping. Typing doesn't wrap: each field behaves like a clock segment, so a digit that would overflow it replaces the field (10, then 2 typed at the start, reads 2 — not 210), and an out-of-range value is clamped when the field loses focus.

Period

Sizes

Five sizes — xs through xl — matching the rest of the family.

Period
Period
Period
Period
Period

Code

<TimeInput size="xs" defaultValue={{ hour: 9, minute: 30 }} />
<TimeInput size="sm" defaultValue={{ hour: 9, minute: 30 }} />
<TimeInput size="md" defaultValue={{ hour: 9, minute: 30 }} />
<TimeInput size="lg" defaultValue={{ hour: 9, minute: 30 }} />
<TimeInput size="xl" defaultValue={{ hour: 9, minute: 30 }} />

Label, description & error

label / description / error / required come from the built-in Field wrapper, same as the rest of the family.

Meeting timeLocal time for all attendees
Period
Meeting time
Period
Pick a time

Code

<TimeInput
  label="Meeting time"
  description="Local time for all attendees"
  defaultValue={{ hour: 9, minute: 30 }}
  required
/>

<TimeInput
  label="Meeting time"
  error="Pick a time"
  defaultValue={{ hour: 0, minute: 0 }}
/>

Controlled

Drive the value from state. onChange hands you the next { hour, minute, second? }.

'use client';
import { useState } from 'react';
import { TimeInput, type TimeValue } from '@42-central/ui-react/time-input';

export function MeetingTime() {
  const [value, setValue] = useState<TimeValue>({ hour: 9, minute: 30 });
  return <TimeInput label="Meeting time" value={value} onChange={setValue} />;
}

Forms

TimeInput has no machine-backed hidden part to piggyback on (unlike Select's hidden <select>), so name renders a hand-rolled hidden HH:mm[:ss] input instead — a <form> submission and FormData pick it up the same way.

<form action={save}>
  <TimeInput name="startTime" defaultValue={{ hour: 9, minute: 0 }} />
</form>

API

Prop

Type

TimeInput has no single focusable control — Hour/Minute/Second/Period are separate sub-fields — so testId lands on the wrapping shell <div> rather than any one of them. See the Testing guide for the full testId convention across the kit.

Accessibility

  • HOUR / MINUTE / SECOND are real role="spinbutton" NumberInputs, each with its own accessible name (Hour / Minute / Second) and the full stepper keyboard model (↑ / ↓, PageUp / PageDown, Home / End). The name stays wired even though the label isn't shown — it's visually hidden (sr-only), not removed, so screen readers still announce it. The h / m / s suffix next to each value is decorative only (aria-hidden), not a replacement accessible name.
  • The AM/PM toggle is a labeled SegmentGroup (a radio-group under the hood) — arrow keys move between the two segments. Its "Period" label is likewise visually hidden but still the accessible name; the segments' own "AM"/"PM" text is visible and self-descriptive.
  • Every stepper wraps at its bounds rather than clamping, so keyboard users can cycle past midnight/the hour without hitting a dead end.
  • The row is one role="group" named by the field's label (through aria-labelledby) and described by its description / error, so a screen reader entering it hears "Meeting time, group" before "Hour". Two TimeInputs labelled "Start time" and "End time" are two distinctly named groups. Without a label the group falls back to "Time" (translations.time), so it's never anonymous. ← / → at the edge of a field's text move to the neighbouring field (and from the last one onto the AM/PM toggle), so the caret crosses the row as if it were one input. Mid-text, the arrows move the caret as usual; right-to-left locales mirror the keys.
  • invalid / error propagate to every sub-control's aria-invalid; label / description / error associate through the built-in Field.

Changelog

v0.19.0
  • A translations prop for the built-in trigger, clear, remove, search, increment/decrement and upload labels

    #331
  • Time-segment and AM/PM groups take their name from the field label, and DatePicker's range rows are announced as "Start time" and "End time"

    #329
v0.16.0
  • DatePicker's typeable works with withTime, and TimeInput moves between fields with the arrow keys

    #250
v0.6.0
  • One size scale, plain xs–xl matching Input's height: size="input-md" becomes size="md"

    #104
  • Inputs drop the filled variant and the color prop

    #96
v0.4.0
  • MultiSelect's searchPlaceholder, DatePicker's timePlaceholder and TimeInput's placeholder

    #45
v0.3.0
  • translations overrides the built-in English microcopy, including in DataTable's date filters

    #24
  • AM/PM and unit suffixes come from Intl for the current locale

    #24
v0.2.0
  • New TimeInput: hour, minute and optional second fields, with format 12 or 24

    #16

On this page