Skip to main content

TimeInput

Overview

The TimeInput component is a form input that opens a popover spinner for time-of-day selection. A clickable launcher icon on the right opens the popover, and you can type directly in the field with segmented keyboard entry. It supports 12-hour and 24-hour formats, optional seconds, custom hour/minute/second increments, min/max bounds, an unselectable-times predicate, and a native <input type="time"> fallback for touch devices.


Import

import { TimeInput } from '@allxsmith/bestax-bulma';

Props

PropTypeDefaultDescription
valueDate | nullControlled selected time (date portion is preserved).
defaultValueDate | nullInitial value for uncontrolled usage.
onChange(d: Date | null) => voidFired when the value changes.
onOpen() => voidFired when the popover opens.
onClose() => voidFired when the popover closes.
minDateEarliest selectable time.
maxDateLatest selectable time.
hourFormat'12' | '24''24'Hour format. '12' shows an AM/PM toggle.
enableSecondsbooleanfalseShow a seconds column. Note: iOS Safari's native time picker UI does not include a seconds wheel; pass mobileNative={false} if you need one on iOS.
incrementHoursnumber1Hour step for the spinner.
incrementMinutesnumber1Minute step. Combine with min/max for slot-style pickers.
incrementSecondsnumber1Second step.
unselectableTimes(d: Date) => booleanPredicate returning true for times that should be skipped. Blocked times are also rejected during manual typing.
placeholderstringPlaceholder text for the input.
formatstring | Intl.DateTimeFormatOptions(see below)Token format string or Intl.DateTimeFormat options.
parse(s: string) => Date | nullCustom parser.
localestringBCP-47 locale tag for Intl formatting.
inlinebooleanfalseRender the spinner inline (no popover).
mobileNativeboolean | 'auto''auto'Use <input type="time"> on coarse-pointer + small-viewport devices.
audioTickbooleanfalsePlay a short audible click on each wheel-item crossing. Substitute for haptic feedback on iOS Safari (which has no web haptic API as of May 2026); on Android, navigator.vibrate(5) fires automatically regardless.
hapticsbooleanfalseAuto-route platform-appropriate feedback: vibrate on Android (already happening), audio thunk on iOS (where vibrate is unavailable). One switch instead of platform-sniffing on the consumer side. audioTick={true} always wins.
editablebooleantrueAllow segmented keyboard typing in the input (type the time directly, auto-advancing across segments). false makes the field picker-only.
popoverbooleantrueWhether the spinner popover exists. false makes the field input-only (segmented typing, no popover).
openOnFocusbooleantrueOpen the popover when the input is focused.
closeOnSelectbooleanfalseClose the popover after a time is selected (off by default).
position'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'auto''bottom-left'Popover anchor position relative to the input.
appendToBodybooleanfalseRender the popover into document.body via portal.
disabledbooleanfalseDisable the input.
readOnlybooleanfalseMake the input read-only.
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'Bulma color modifier.
size'small' | 'medium' | 'large'Size variant.
isRoundedbooleanfalseRender the input with rounded corners.
iconLeftNamestring'clock'Decorative left icon glyph for the wrapping Control (shown by default). Set '' to hide.
triggerIconbooleantrueShow a clickable launcher button on the right that toggles the popover.
triggerIconNamestring'chevron-down'Glyph for the right launcher button.
namestringForm field name.
formstringForm id the input belongs to.
requiredbooleanfalseMarks the input as required.
labelReact.ReactNodeField label.
horizontalbooleanfalseRender the field with horizontal layout.
messageReact.ReactNodeHelp/validation text below the input.
messageColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'Color modifier for the help message.
classNamestringAdditional CSS classes for the input.
refReact.Ref<HTMLInputElement>Forwarded to the underlying <input>.
...All standard HTML and Bulma helper props(See Helper Props)

The default format depends on hourFormat and enableSeconds: 'HH:mm', 'HH:mm:ss', 'hh:mm A', or 'hh:mm:ss A'.

When you pass an explicit token format, that format is the source of truth for the wheel spinner: a 12-hour format (h/hh with A/a) shows a 12-hour wheel with an AM/PM column, and a 24-hour format (H/HH) shows a 24-hour wheel — regardless of hourFormat. So hourFormat only matters when you don't pass a format. (If format is an Intl.DateTimeFormat options object rather than a token string, the cycle can't be read from it and the wheel falls back to hourFormat.)


Usage

Basic TimeInput

<TimeInput label="Time" placeholder="HH:MM" />

Typing-first — the same example with openOnFocus={false}: focusing or clicking the field lets you type; open the popover with the launcher icon (or press ).

<TimeInput label="Time" placeholder="HH:MM" openOnFocus={false} />


Controlled

function example() {
  const [v, setV] = useState(() => {
    const d = new Date();
    d.setHours(13, 45, 0, 0);
    return d;
  });
  return (
    <Block>
      <TimeInput label="Departure" value={v} onChange={setV} />
      <Paragraph mt="2">Selected: {v ? v.toLocaleTimeString() : '—'}</Paragraph>
    </Block>
  );
}

Typing-first — the same controlled example with openOnFocus={false}: focus and type freely; the launcher icon on the right (or ) opens the popover.

function example() {
  const [v, setV] = useState(() => {
    const d = new Date();
    d.setHours(13, 45, 0, 0);
    return d;
  });
  return (
    <Block>
      <TimeInput
        label="Departure"
        value={v}
        onChange={setV}
        openOnFocus={false}
      />
      <Paragraph mt="2">Selected: {v ? v.toLocaleTimeString() : '—'}</Paragraph>
    </Block>
  );
}


12-hour Format

hourFormat="12" renders an AM/PM toggle column.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="12-hour"
      hourFormat="12"
      defaultValue={v}
      mobileNative={false}
    />
  );
}

Forced to the custom wheel

The OS-native pickers use the device's clock setting (12h/24h), so hourFormat is ignored there. This example forces mobileNative={false} so the 12-hour format shows on touch devices too.

Typing-first — identical, but with openOnFocus={false} so focusing lets you type (press a / p on the AM/PM segment); the launcher icon (or ) opens the popover.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="Press a / A / p / P on AM-PM"
      hourFormat="12"
      defaultValue={v}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


24-hour Format

The default. Hours run 00–23.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="24-hour"
      hourFormat="24"
      defaultValue={v}
      mobileNative={false}
    />
  );
}

Forced to the custom wheel

Same as above — the OS-native pickers follow the device clock setting. This example forces mobileNative={false} to show 24-hour on touch devices.

Typing-first — the same 24-hour example with openOnFocus={false}: click in and type the time; the launcher icon (or ) brings up the popover.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="24-hour"
      hourFormat="24"
      defaultValue={v}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


With Seconds

Adds a third spinner column.

function example() {
  const v = new Date();
  v.setHours(10, 20, 30, 0);
  return (
    <TimeInput
      label="With seconds"
      enableSeconds
      defaultValue={v}
      mobileNative={false}
    />
  );
}

iOS Safari has no seconds wheel (Android Chrome does)

This example forces mobileNative={false} so the seconds wheel shows on every device. Android Chrome renders a seconds spinner in its native time picker when step < 60 (which our component sets when enableSeconds is true). iOS Safari has no seconds wheel under any circumstances — Apple's native picker UI is hard-locked to hour/minute spinners regardless of step. The input value can carry seconds entered programmatically, but iOS users can't pick them in the wheel. If you need a seconds wheel on iOS, pass mobileNative={false} to force the custom wheel popover. See Mobile Native below for the full iOS-vs-Android picker support matrix.

Typing-first — the same example plus openOnFocus={false}: type across the hours / minutes / seconds segments, and open the popover with the launcher icon (or ).

function example() {
  const v = new Date();
  v.setHours(10, 20, 30, 0);
  return (
    <TimeInput
      label="Tab through hours / minutes / seconds"
      enableSeconds
      defaultValue={v}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


Increment Steps

Pair incrementMinutes with min/max for slot-style pickers.

function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="15-minute slots"
      incrementMinutes={15}
      defaultValue={v}
      mobileNative={false}
    />
  );
}

Forced to the custom wheel

iOS Safari shows every minute regardless of step. This example forces mobileNative={false} so the 15-minute stepping is enforced on every device.

Typing-first — the same example with openOnFocus={false}: increments only step the wheels, not typing — typed values are free-grained — so open the stepped wheels with the launcher icon (or ).

function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="15-minute wheels — typing is free-grained"
      incrementMinutes={15}
      defaultValue={v}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


Min and Max

Restrict to a time-of-day range.

function example() {
  const today = new Date();
  const min = new Date(today);
  min.setHours(9, 0, 0, 0);
  const max = new Date(today);
  max.setHours(17, 0, 0, 0);
  const dv = new Date(today);
  dv.setHours(12, 0, 0, 0);
  return (
    <TimeInput
      label="Office hours: 09:00 — 17:00"
      min={min}
      max={max}
      defaultValue={dv}
    />
  );
}

iOS native does not enforce min/max in the picker

On iOS Safari the picker UI lets the user spin to any time; min/max only fire at form-submission validation (WebKit bug #225639, still open). Pass mobileNative={false} for iOS-side enforcement. Android Chrome's native picker does honor them.

Typing-first — the same bounds with openOnFocus={false}: typed entry is clamped to the window just like the wheels; open the popover with the launcher icon (or ).

function example() {
  const at = (h, m) => {
    const d = new Date();
    d.setHours(h, m, 0, 0);
    return d;
  };
  return (
    <TimeInput
      label="Typed entry clamped to 09:00 – 17:00"
      min={at(9, 0)}
      max={at(17, 0)}
      defaultValue={at(12, 0)}
      openOnFocus={false}
    />
  );
}


Unselectable Times

Skip times that match a predicate. Blocked values render dimmed in the wheel so the constraint is visible, the per-item button gets the disabled attribute (so clicks no-op), and keyboard / wheel scrolling automatically advances past them via the nextValid lookahead. The disabled state is dynamic — it's evaluated against the other columns' current values, so e.g. a predicate that blocks only 12:00–12:59 leaves all minutes enabled once the user moves the hour off 12. Manual typing also rejects blocked times, the same way min/max are enforced.

function example() {
  const v = new Date();
  v.setHours(11, 30, 0, 0);
  return (
    <TimeInput
      label="Lunch hour blocked"
      unselectableTimes={d => d.getHours() === 12}
      defaultValue={v}
      mobileNative={false}
    />
  );
}

Forced to the custom wheel

HTML has no predicate equivalent, so the OS-native pickers can't block any times. This example forces mobileNative={false} so the rule works on touch devices; in your app keep mobileNative="auto" and also validate in onChange.

Typing-first — the same predicate with openOnFocus={false}: typing or arrowing into a blocked time is rejected just like in the wheels, and the launcher icon (or ) opens the popover.

function example() {
  const v = new Date();
  v.setHours(11, 30, 0, 0);
  return (
    <TimeInput
      label="Lunch hour rejected while typing"
      unselectableTimes={d => d.getHours() === 12}
      defaultValue={v}
      mobileNative={false}
      openOnFocus={false}
    />
  );
}


Manual Keyboard Entry

Focus the input — the hours segment highlights automatically and the keyboard alone can drive the full time entry without ever opening the popover. Segment mode activates whenever the format is a token string with padded tokens (HH, hh, mm, ss, A, a); custom Intl.DateTimeFormatOptions formats and unpadded tokens (H, h, m, s) fall back to free-form text entry.

These examples use openOnFocus={false} so the popover doesn't cover the input — set openOnFocus={true} (the default) and both UIs coexist. To turn segment typing off entirely, pass editable={false} (picker-only); to drop the popover and keep only the field, pass popover={false} (input-only).

Opening the picker vs. typing

With openOnFocus={false} (used here), clicking the field just lets you type — the popover does not appear on focus or click. Open the picker by clicking the launcher icon on the right (or pressing ). With the default openOnFocus={true}, focusing or clicking the field opens the popover immediately (you can still type while it's open).

Basic

Click into the field, then press / to change the hours, to move to minutes, and continue.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="Click in, then use arrow keys"
      defaultValue={v}
      openOnFocus={false}
    />
  );
}


Digit entry and auto-advance

Type digits to overwrite the active segment. The picker auto-advances when no further digit could form a valid value — for 24-hour hours, typing 5 advances immediately (no 50–59 to wait for), but typing 2 waits for an optional second digit (2023 are still valid). Minutes/seconds advance after any digit 69, since 60+ is out of range.

function example() {
  const v = new Date();
  v.setHours(9, 0, 0, 0);
  return (
    <TimeInput
      label="Type digits — auto-advance after each segment"
      defaultValue={v}
      openOnFocus={false}
    />
  );
}


Controlled with live value

The value updates on every increment, digit, or AM/PM toggle — exactly like the wheel popover. Bind to useState to see it.

function example() {
  const [v, setV] = useState(() => {
    const d = new Date();
    d.setHours(13, 45, 0, 0);
    return d;
  });
  return (
    <Block>
      <TimeInput
        label="Arrow or type — value updates live"
        value={v}
        onChange={setV}
        openOnFocus={false}
      />
      <Paragraph mt="2">Selected: {v ? v.toLocaleTimeString() : '—'}</Paragraph>
    </Block>
  );
}


Free-form fallback

When format is an Intl.DateTimeFormatOptions object (or uses unpadded tokens like H:m), segment mode silently disables — focusing the input does not highlight a segment, arrow keys fall through to default behavior, and the input parses on blur instead.

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="Free-form (Intl format)"
      format={{ hour: '2-digit', minute: '2-digit' }}
      defaultValue={v}
      openOnFocus={false}
    />
  );
}


12-hour with seconds

The full segment walk: hhmmss → AM/PM. / move through all four segments, digits overwrite with auto-advance, and a / p toggle the meridiem.

function example() {
  const v = new Date();
  v.setHours(13, 45, 30, 0);
  return (
    <TimeInput
      label="hh:mm:ss + AM/PM"
      hourFormat="12"
      enableSeconds
      defaultValue={v}
      openOnFocus={false}
    />
  );
}


Haptic-feel Feedback (audio tick + band pulse)

The custom-wheel popover fires three kinds of feedback per item crossing to make the picker feel tactile across platforms:

  • Haptic (Android only)navigator.vibrate(5) fires unconditionally on Android Chrome / Firefox Android / Samsung Internet. iOS Safari does not expose navigator.vibrate, and Apple patched the <input type="checkbox" switch> workaround in iOS 26.5 — there is no longer any web-only path to the Taptic Engine.
  • Audio thunk (opt-in via audioTick) — a 160 Hz triangle-wave tone sweeping down to 110 Hz over ~30 ms. The low fundamental and downward pitch sweep are tuned to read as a body-felt thump rather than an ear-felt beep, matching the proprioceptive signature of native Taptic UI pops.
  • Visual band pulse (always-on) — the selection band briefly scales (1 → 1.04 → 1) and brightens (× 1.15) over 110 ms on every tick. Single-element Web Animations API call with composite: 'replace' so rapid fling ticks restart cleanly. Respects prefers-reduced-motion: reduce.
function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="Audio thunk + band pulse"
      audioTick
      mobileNative={false}
      defaultValue={v}
    />
  );
}

Typing-first — the same example with openOnFocus={false}: typing stays silent — the audio thunk and band pulse fire once you open the wheels via the launcher icon (or ) and scroll them.

function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="Audio thunk + band pulse"
      audioTick
      mobileNative={false}
      defaultValue={v}
      openOnFocus={false}
    />
  );
}

For a single switch that picks the right feedback per platform automatically — vibrate on Android, audio thunk on iOS — use haptics instead of audioTick:

function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="Auto-routed haptics"
      haptics
      mobileNative={false}
      defaultValue={v}
    />
  );
}

Typing-first — the same auto-routed feedback with openOnFocus={false}: the vibrate/thunk fires when you open the wheels with the launcher icon (or ) and scroll them, not while typing.

function example() {
  const v = new Date();
  v.setHours(9, 30, 0, 0);
  return (
    <TimeInput
      label="Auto-routed haptics"
      haptics
      mobileNative={false}
      defaultValue={v}
      openOnFocus={false}
    />
  );
}

haptics feature-detects navigator.vibrate rather than UA-sniffing. On Android (vibrate present), no audio is layered on top of the real haptic; on iOS (vibrate absent), the audio thunk fills in.

Behaviour details
  • audioTick is off by default; opt in per-instance.
  • Android still fires haptic via navigator.vibrate independently — enabling audioTick adds the audible thunk on top.
  • The visual band pulse is always-on and gated only by prefers-reduced-motion; no prop needed.
  • The first wheel touch unlocks the AudioContext (Web Audio's gesture requirement); subsequent ticks play with no further user action.
  • The iPhone silent switch (and Android system volume) suppress the tick audio — matching native UX expectations.
  • Native iOS wrappers (Capacitor, React Native + WebView) can bridge to UIImpactFeedbackGenerator for real Taptic feedback; the audio thunk + visual pulse together are the closest pure-web equivalent.

Inline

Render the spinner directly without a popover.

function example() {
  const v = new Date();
  v.setHours(8, 30, 0, 0);
  return <TimeInput label="Inline" inline defaultValue={v} />;
}


Mobile Native

By default mobileNative='auto': on touch devices with a small viewport ((pointer: coarse) and (max-width: 768px)) the input swaps to a plain <input type="time"> so the OS-native time picker handles the interaction. On desktop the custom wheel popover renders. Pass true or false to override the auto-detection.

Native picker support varies — iOS lags Android

The OS-native fallback is just a <input type="time">, so it inherits each platform's behavior. The custom wheel popover (mobileNative={false}) honors every prop on every device.

Honored on Android Chrome but NOT on iOS Safari:

  • min / max — Android dims out-of-range times in the wheel; iOS lets the user spin to any value, only firing the constraint at form-submission validation. (WebKit bug #225639, still open as of 2026.)
  • incrementMinutes, incrementHours, incrementSeconds — Android Chrome respects step (e.g. only 0/15/30/45 minutes selectable when step=900). iOS shows every value regardless.
  • enableSeconds — Android Chrome shows a seconds spinner when step < 60. iOS has no seconds wheel under any circumstances.

Ignored on BOTH iOS Safari and Android Chrome (HTML-spec gaps):

  • unselectableTimes — HTML has no predicate equivalent; native pickers can't evaluate functions.
  • hourFormat — both use the device's system clock setting (12h/24h).
  • format, locale — both use the device's system locale; per-input overrides are ignored.
  • placeholder — neither renders placeholder text on time inputs.

If any of these matter, pass mobileNative={false} to force the custom wheel popover (works on every device), or duplicate the constraint in onChange / server-side validation.

Force native

<TimeInput label="Always native" mobileNative={true} />

Force the custom wheel (even on mobile)

Useful when you want the same wheel UI everywhere — for example, if your app already provides a touch-friendly viewport-sized picker.

<TimeInput label="Always custom wheel" mobileNative={false} />


Formats

The format prop takes a token string or Intl.DateTimeFormatOptions. The same time rendered several ways:

function example() {
  const v = new Date();
  v.setHours(13, 45, 30, 0);
  return (
    <Block display="flex" flexDirection="column" gap="4">
      <TimeInput label="HH:mm (24h, default)" defaultValue={v} />
      <TimeInput label="hh:mm A (12h)" format="hh:mm A" defaultValue={v} />
      <TimeInput
        label="hh:mm a (lowercase)"
        format="hh:mm a"
        defaultValue={v}
      />
      <TimeInput label="HH:mm:ss" format="HH:mm:ss" defaultValue={v} />
      <TimeInput label="hh:mm:ss A" format="hh:mm:ss A" defaultValue={v} />
    </Block>
  );
}

Typing-first — the same token formats with openOnFocus={false} on every instance: type into the segments each format defines; the launcher icon (or ) opens the popover.

function example() {
  const v = new Date();
  v.setHours(13, 45, 30, 0);
  return (
    <Block display="flex" flexDirection="column" gap="4">
      <TimeInput
        label="HH:mm (24h, default)"
        defaultValue={v}
        openOnFocus={false}
      />
      <TimeInput
        label="hh:mm A (12h)"
        format="hh:mm A"
        defaultValue={v}
        openOnFocus={false}
      />
      <TimeInput
        label="hh:mm a (lowercase)"
        format="hh:mm a"
        defaultValue={v}
        openOnFocus={false}
      />
      <TimeInput
        label="HH:mm:ss"
        format="HH:mm:ss"
        defaultValue={v}
        openOnFocus={false}
      />
      <TimeInput
        label="hh:mm:ss A"
        format="hh:mm:ss A"
        defaultValue={v}
        openOnFocus={false}
      />
    </Block>
  );
}

Padded token formats support segmented typing; the meridiem (A/a) and seconds (ss) segments appear when the format includes them. Token formats render the same regardless of locale; pass an Intl.DateTimeFormatOptions object for locale-aware output (display-only unless you add a custom parse).

function example() {
  const v = new Date();
  v.setHours(13, 45, 0, 0);
  return (
    <TimeInput
      label="Intl, fr-FR (display only)"
      format={{ hour: '2-digit', minute: '2-digit' }}
      locale="fr-FR"
      editable={false}
      defaultValue={v}
      mobileNative={false}
    />
  );
}

Forced to the custom wheel

format and locale are ignored by the OS-native pickers (they use the device locale). This example forces mobileNative={false} so the locale-aware display shows on touch devices too.


Launcher Icon

A clickable launcher sits on the right and toggles the popover — handy for input-mode (openOnFocus={false}) where you type the value and click the icon to open the spinner. Override its glyph with triggerIconName, or hide it with triggerIcon={false} (the popover still opens on focus / click). The decorative left icon is independent: it shows by default, takes its glyph from iconLeftName, and is hidden with iconLeftName="".

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Default (left icon + right launcher)" />
  <TimeInput label="Custom launcher glyph" triggerIconName="hourglass" />
  <TimeInput label="No launcher" triggerIcon={false} />
  <TimeInput label="Left icon hidden" iconLeftName="" />
</Block>

Typing-first — the same set with openOnFocus={false} on each instance: the field is type-first, which makes the right launcher icon (or ) the way into the popover.

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Default (left icon + right launcher)" openOnFocus={false} />
  <TimeInput
    label="Custom launcher glyph"
    triggerIconName="hourglass"
    openOnFocus={false}
  />
  <TimeInput
    label="No launcher (press ↓ to open)"
    triggerIcon={false}
    openOnFocus={false}
  />
  <TimeInput label="Left icon hidden" iconLeftName="" openOnFocus={false} />
</Block>


Sizes

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Small" controlSize="small" size="small" />
  <TimeInput label="Default" />
  <TimeInput label="Medium" controlSize="medium" size="medium" />
  <TimeInput label="Large" controlSize="large" size="large" />
</Block>

Typing-first — the same sizes with openOnFocus={false} on every instance: focusing lets you type, and the launcher icon (or ) opens the popover.

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput
    label="Small"
    controlSize="small"
    size="small"
    openOnFocus={false}
  />
  <TimeInput label="Default" openOnFocus={false} />
  <TimeInput
    label="Medium"
    controlSize="medium"
    size="medium"
    openOnFocus={false}
  />
  <TimeInput
    label="Large"
    controlSize="large"
    size="large"
    openOnFocus={false}
  />
</Block>


Colors

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Primary" color="primary" />
  <TimeInput label="Info" color="info" />
  <TimeInput label="Success" color="success" />
  <TimeInput label="Warning" color="warning" />
  <TimeInput label="Danger" color="danger" />
</Block>

Typing-first — the same colors with openOnFocus={false} everywhere: click in to type, and use the launcher icon (or ) to open the popover.

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Primary" color="primary" openOnFocus={false} />
  <TimeInput label="Info" color="info" openOnFocus={false} />
  <TimeInput label="Success" color="success" openOnFocus={false} />
  <TimeInput label="Warning" color="warning" openOnFocus={false} />
  <TimeInput label="Danger" color="danger" openOnFocus={false} />
</Block>


States

<Block display="flex" flexDirection="column" gap="4">
  <TimeInput label="Disabled" disabled />
  <TimeInput label="Read only" readOnly defaultValue={new Date()} />
  <TimeInput label="Loading" isLoading />
</Block>


Context-Aware Rendering

The TimeInput component is context-aware: it detects whether it is already inside a Field and adjusts its rendering accordingly.

Default (with label)

<TimeInput label="Time" placeholder="HH:MM" />


With Field Wrapper

function example() {
  return (
    <Field horizontal label="Time">
      <Field.Body>
        <Field>
          <TimeInput placeholder="HH:MM" />
        </Field>
      </Field.Body>
    </Field>
  );
}


With Field and Control Wrappers

function example() {
  return (
    <Field horizontal label="Time">
      <Field.Body>
        <Field>
          <Control iconLeftName="clock">
            <TimeInput placeholder="HH:MM" />
          </Control>
        </Field>
      </Field.Body>
    </Field>
  );
}


Keyboard Navigation

On the input (segmented entry)

Focus the input — the hours segment is automatically highlighted. Segment mode activates whenever the format is a token string with padded tokens (HH, hh, mm, ss, A, a); Intl-options formats and variable-width tokens (H, h, m, s) fall back to free-form text entry.

KeyAction
/ Increment / decrement the active segment (wraps at boundaries)
/ Move to previous / next segment (hour ↔ minute ↔ second ↔ AM/PM)
09Overwrite the active segment; auto-advances when no further digit is valid
a / A / p / PToggle AM/PM on the meridiem segment
BackspaceClear the typed-digit buffer; if already cleared, move to the previous segment
TabClear segment selection so focus moves out naturally
EscapeClose popover
EnterClose popover when closeOnSelect={true} (value is already committed live)

On a wheel column (when the popover is open)

KeyAction
/ Increment / decrement focused column
PageUp / PageDownIncrement / decrement by 5×
TabMove focus to next column
EnterCommit live value, close popover

Form Submission

TimeInput participates in HTML form submission. Pass a name and the value is forwarded as HH:MM (or HH:MM:SS if enableSeconds).

PropDescription
nameForm field name.
formOptional id of the form the input belongs to.
requiredMarks the field as required for native HTML form validation.
function TimeInputFormDemo() {
  const [submitted, setSubmitted] = React.useState('');
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2));
      }}
    >
      <TimeInput name="appointment" label="Appointment time" required />
      <div style={{ marginTop: '1rem' }}>
        <button type="submit" className="button is-primary">
          Submit
        </button>
      </div>
      {submitted && <pre style={{ marginTop: '1rem' }}>{submitted}</pre>}
    </form>
  );
}


Accessibility

  • Trigger uses role="combobox" with aria-haspopup="dialog", aria-expanded, and aria-controls.
  • Popover panel has role="dialog" with an accessible name.
  • Each spinner column has role="spinbutton" with aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext.
  • AM/PM toggle exposes aria-pressed.
  • Honors prefers-reduced-motion.


Additional Resources

Pro Tip

Combine incrementMinutes={5} (or 15/30) with min and max to build a tight slot picker — the spinner will only land on valid slots.