Skip to main content

TimePicker

Two small fields, hours and minutes, that move the caret along as you type. There is no clock, no drop-down and — despite the 12-hour option — no AM/PM control.

TimePickerTimePicker

Use this when / not when​

  • Use for a time typed beside a date, or on its own where the date does not matter.
  • Not on its own for a 12-hour interface. The component formats hours 1–12 but renders nothing to choose AM or PM; you supply meridiem and build that control. DateTimePicker is the one that already has it.
  • Not for a duration. The value comes back as a point in time, carrying a date.
  • Not where the value must be driven from outside: initialTime is read once.

Import​

import { TimePicker } from "@onlyoffice/apps-ui-kit/components/time-picker";

Also exported from the root barrel @onlyoffice/apps-ui-kit.

Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme.

Times come back as Luxon DateTime objects.

Stories​

Default​

A 24-hour picker preset to 10:30 (initialTime); type 9 in the hours field and watch it become 09 and jump to minutes; change any other prop live in the Controls panel below.

DefaultDefault

With Error​

The border turns red to flag a time the form rejected (hasError); the fields stay editable so it can be corrected in place.

With ErrorWith Error

Twelve Hour Format​

Hours stop at 12 (isTwelveHourFormat); nothing on screen tells AM from PM, only the meridiem behind each box decides what onChange receives: the first reports 10:30, the second, showing 02:30, reports 14:30.

Twelve Hour FormatTwelve Hour Format

Focus On Render​

The picker opens with the hours field selected (focusOnRender), so a form that asks for a time first takes the digits without a click; type 14 and the caret moves on to minutes.

Focus On RenderFocus On Render

Css Customization​

The TimePicker and inner TextInput variables set on one wrapper -- the variables are listed under CSS variables on this page. The first box shows the border, background, size and radius variables and the inner fields' text colour; click into it to see --time-input-focus-border. The second adds hasError, the only state in which --time-input-error-border has anything to colour. --text-input-bg is set to the same value as --time-input-bg so the fields blend into the box.

Css CustomizationCss Customization

Minimal example​

onChange gives a full DateTime: the date from initialTime, the time from the fields.

import { useState } from "react";
import { DateTime } from "luxon";
import { TimePicker } from "@onlyoffice/apps-ui-kit/components/time-picker";

export function ReminderTime({ day }: { day: DateTime }) {
const [at, setAt] = useState(day);

return (
<div>
<TimePicker initialTime={day} onChange={setAt} />
<p>Reminder at {at.toFormat("HH:mm")}</p>
</div>
);
}

Props​

PropertyTypeDescription
className?stringApplied to the outermost element. Default: "".
classNameInput?stringPrefix for the two fields' class names: they become <classNameInput>-hours-input and -minutes-input. Left out, the fields get no extra class.
focusOnRender?booleanWhether the hours field is selected on mount. Default: false.
forwardedRef?RefObject<HTMLDivElement | null>Ref to the outermost element.
hasError?booleanWhether the group is drawn in its error colours; they stay while a field is focused. Default: false.
initialTime?Date | DateTime<boolean> | stringTime the fields start on. It is read once, on mount; changing it afterwards does nothing. Its date part is carried into every value onChange reports, so pass a full date-time if that matters. Midnight today is used when it is left out.
isTwelveHourFormat?booleanWhether hours run 1 to 12 rather than 0 to 23. It changes the fields only — no AM/PM control is rendered — and requires meridiem.
meridiem?string"AM" or "PM", used when parsing the typed time in 12-hour mode. You own this value and the control that changes it.
onBlur?() => voidCalled when typing completes the minutes field — two digits, a single digit above 5, a third digit or a value above 59 — not when the field loses focus.
onChange?(date: DateTime) => voidCalled on every accepted keystroke with a full DateTime — the date from initialTime combined with the typed time. Default: () => {}.
tabIndex?numberPosition of both fields in the tab order. Left out, both take their natural place in it.
testId?stringdata-testid of the outermost element. Default: "time-picker".

Recipes​

Error​

hasError recolours the group around both fields. It prints no message, and there is no validation of its own beyond rejecting impossible numbers.

import { useState } from "react";
import { DateTime } from "luxon";
import { TimePicker } from "@onlyoffice/apps-ui-kit/components/time-picker";
import { Text } from "@onlyoffice/apps-ui-kit/components/text";

export function OfficeHours({ day }: { day: DateTime }) {
const [at, setAt] = useState(day.set({ hour: 9, minute: 0 }));
const isOutside = at.hour < 9 || at.hour >= 18;

return (
<div>
<TimePicker initialTime={at} onChange={setAt} hasError={isOutside} />
{isOutside ? (
<Text fontSize="12px">Choose a time between 09:00 and 18:00</Text>
) : null}
</div>
);
}

A 12-hour clock​

The component will format hours 1–12, but the meridiem is yours: hold it, render the control, and pass it in so the typed time is parsed into the right half of the day.

import { useState } from "react";
import { DateTime } from "luxon";
import {
ComboBox,
type TOption,
} from "@onlyoffice/apps-ui-kit/components/combobox";
import { TimePicker } from "@onlyoffice/apps-ui-kit/components/time-picker";

const HALVES: TOption[] = [
{ key: "AM", label: "AM" },
{ key: "PM", label: "PM" },
];

export function TwelveHourTime({ day }: { day: DateTime }) {
const [at, setAt] = useState(day);
const [half, setHalf] = useState<TOption>(HALVES[0]);

return (
<div style={{ display: "flex", gap: 8 }}>
<TimePicker
initialTime={day}
onChange={setAt}
isTwelveHourFormat
meridiem={String(half.key)}
/>
<ComboBox
options={HALVES}
selectedOption={half}
onSelect={setHalf}
scaledOptions
/>
<span>{at.toFormat("hh:mm a")}</span>
</div>
);
}

Behaviour the types don't state​

  • The value carries a date. onChange hands back a DateTime built from initialTime's date plus the typed time, so without initialTime every value is on today's date at the typed hour.
  • initialTime is read once. There is no effect syncing it, so the fields cannot be driven from outside after mount; remount with a key if they must be.
  • Typing moves the caret for you. Two digits in hours jumps to minutes, and a first digit that cannot start a valid hour — above 2, or above 1 in 12-hour mode — is padded with a zero and jumps as well. The same happens between minutes and blur.
  • Out-of-range input is dropped, not corrected. Minutes above 59 call onBlur and leave the field as it was; letters are ignored entirely. A second hour digit that would pass 23 (12 in 12-hour mode) is dropped: the first digit stays, padded to 0x, and the caret moves to minutes.
  • Clearing a field sets it to 00 and reports that time, so neither field can be left empty.
  • A single digit is zero-padded on blur, so 9 becomes 09.
  • isTwelveHourFormat renders no AM/PM control. It changes the hour format and the maximum hour; meridiem is a plain string you pass, and the types require it alongside isTwelveHourFormat, because without it the typed time cannot be parsed and onChange is never called.
  • classNameInput is a prefix, not a class. The fields get <classNameInput>-hours-input and -minutes-input; leave the prop out and they get no extra class.
  • The error colour survives focus. With hasError the border keeps the error colour while a field is being edited, instead of switching to the focus colour.
  • The right-click menu is suppressed on both fields.
  • The group is always laid out left to right, hours then minutes, even inside a right-to-left interface: the wrapper sets direction: ltr.
  • The two fields are separated by a literal : text node, and clicking anywhere in the group that is not the minutes field selects the hours.

CSS variables​

Set these on any ancestor; the group reads them through a fallback to the theme token or to a fixed size.

VariableDefaultEffect
--time-input-border--input-border-colorBorder colour at rest
--time-input-bg--input-bgBackground of the group
--time-input-focus-border--input-border-focusBorder colour while either field is focused
--time-input-error-border--input-error-borderBorder colour under hasError, kept while a field is focused
--time-input-width60pxWidth of the group
--time-input-height32pxHeight of the group
--time-input-radius3pxBorder radius
--time-input-padding0px 6pxPadding inside the group, as a padding shorthand

The two fields inside are TextInputs drawn without a border, so its own variables reach them too: --text-input-color sets the digits' colour, and --text-input-bg their background, which is painted over the group's and should be set to the same value as --time-input-bg for the fields to blend in.

Accessibility​

  • Both fields are real inputs with aria-labels of "Hours" and "Minutes", and inputMode="numeric" brings up a number keypad. Tab reaches both fields on its own; tabIndex moves them in the order and reaches both.
  • role="group" and aria-label="Time picker" are hardcoded English on the wrapper and cannot be overridden.
  • The automatic jump between fields moves focus while the user is typing, which is disorienting with a screen reader and makes correcting a mistake awkward.
  • hasError is a colour only — it sets no aria-invalid and prints no message.
  • Suppressing the context menu removes paste-by-right-click.

Test ids​

Elementdata-testid
The grouptime-picker, or testId
Hours fieldhours-input
Minutes fieldminutes-input

The two fields carry theirs as data-test-id, with a hyphen, not data-testid; only the group uses the usual attribute.