Gecko UIGecko UI

TimeInput

A time, typed segment by segment or picked from a column for each

TimeInput

A time, typed segment by segment or picked from a column for each.

Installation

import { TimeInput } from '@geckoui/geckoui';

Basic Usage

:

value: "14:30"

const [time, setTime] = useState<string | null>("14:30");

<TimeInput value={time} onChange={setTime} />

value is always 24 hour HH:mm, or HH:mm:ss when the format asks for seconds. It sorts and compares as it is, so two times can be checked against each other without being parsed. format decides only what is on screen.

onChange is called with null while the time is incomplete or the rule turns it down.

Props API

PropTypeDefaultDescription
valuestring | null-The time as 24 hour HH:mm, or HH:mm:ss with seconds
onChange(value: string | null) => void-Called with the new time, or null while it is not one
onSubmit() => void-Called once the last segment is filled
format'HH:mm' | 'hh:mm A' | 'HH:mm:ss' | 'hh:mm:ss A''HH:mm'What is shown
stepnumber1Minutes between the entries in the minute column
disabledTime({ hour, minute, second }) => boolean-Which times cannot be chosen
disabledbooleanfalse
readOnlybooleanfalse
aria-invalidbooleanfalse
prefixFC | ReactNode-
suffixFC | ReactNode-
placeholderstringthe format
hideClearIconbooleanfalse
hideClockIconbooleanfalse
classNamestring-CSS class for the field
wrapperClassNamestring-CSS class for the wrapper
listClassNamestring-CSS class for the picker
listPlacementPlacement'bottom-start'Where the picker opens
floatingStrategyStrategy'absolute'

Examples

Formats

:
:
::
::

value: "16:05:30"

<TimeInput value={time} onChange={setTime} format="HH:mm" />
<TimeInput value={time} onChange={setTime} format="hh:mm A" />
<TimeInput value={time} onChange={setTime} format="HH:mm:ss" />
<TimeInput value={time} onChange={setTime} format="hh:mm:ss A" />

All four share one value. A 12 hour field shows 04:05 PM and still reports "16:05".

Each segment gets a column of its own, so a 12 hour field with seconds opens four.

Slots

Pick a slot
:
<TimeInput value={time} onChange={setTime} step={30} />

step is the minutes between entries in the minute column. It lists every minute by default, the way the platform's own picker does. Typing is unaffected: you can still type 09:07 with step={30}.

Ruling times out

hh:mm A
:

value: null

<TimeInput
  value={time}
  onChange={setTime}
  format="hh:mm A"
  step={30}
  disabledTime={({ hour }) => hour < 9 || hour >= 17 || hour === 13}
/>

disabledTime is given 24 hour numbers whatever the format shows, so a rule is written once and holds for every format.

A time it turns down sends null to onChange, whether it was picked or typed.

Cells are greyed out when nothing they could become is allowed, reading the columns to their left as settled and leaving the ones to their right free. So an hour rule greys out hours straight away but leaves the minutes alone until an hour is chosen.

Allowing two separate windows

hh:mm A
:
<TimeInput
  value={time}
  onChange={setTime}
  format="hh:mm A"
  disabledTime={({ hour }) => ![1, 2, 16, 17].includes(hour)}
/>

Only 1 and 2 in the morning, 4 and 5 in the afternoon.

This is why the columns only read leftwards. If every column answered to every other, choosing PM would grey out 01 and 02, while an hour of 04 would grey out AM, and neither could be changed without emptying the field. The hour column is never greyed out on account of the meridiem, so there is always a way back.

Picking 01 while PM is still set leaves a pair the rule turns down. onChange gets null, and the stale segment shows in the picker as chosen but unavailable.

States

:
:
:
<TimeInput value="09:00" disabled />
<TimeInput value="09:00" readOnly />
<TimeInput value="09:00" aria-invalid />

Neither a disabled nor a read-only field opens the picker.

Next to a date

DateInput and TimeInput are separate fields, each holding a plain string:

const [date, setDate] = useState<string | null>(null);   // "2026-09-19"
const [time, setTime] = useState<string | null>(null);   // "16:30"

<div className="flex gap-3">
  <DateInput value={date} onChange={setDate} className="flex-1" />
  <TimeInput value={time} onChange={setTime} className="w-44" />
</div>

const startsAt = date && time ? `${date}T${time}` : null;

The keyboard

KeyDoes
0–9Fill the segment, moving on once it cannot take another digit
↑ ↓Step the segment under the caret, wrapping at its ends
← →Move between segments
A PSet AM or PM
BackspaceEmpty the segment, or move back when it already is
EscClose the picker

Typing 3 into a 24 hour field settles it at 03 and moves on, because no hour starts with 3.

Styling with CSS

VariableDefaultApplies to
--gecko-time-input-column-height15remHow tall each column scrolls
--gecko-time-input-column-width4remHow wide each column is
--gecko-time-input-z10The picker's stacking order
.GeckoUITimeInputWrapper {
  --gecko-time-input-column-height: 20rem;
}

Class names and data attributes

  • GeckoUITimeInputWrapper - Holds the field and the picker
  • GeckoUITimeInput - The field, with data-state, aria-invalid, data-empty and data-focus
  • GeckoUITimeInput__segment - One segment, with data-segment and data-empty
  • GeckoUITimeInput__picker - The dropdown
  • GeckoUITimeInput__column - One column, labelled by its segment
  • GeckoUITimeInput__cell - One option, with data-state of selected or unselected