Skip to main content
urbicon ui

TimeInputbeta

A field for entering a time of day: hour, minute and optional seconds cells, stepped with the Arrow keys and shown in 12- or 24-hour. The bound value is a 24-hour string (HH:MM, or HH:MM:SS with seconds) once the time is complete, null while it is half-typed, and undefined while the field is empty.

Playground

Time
Format
Size Style variant
Variant Style variant
Tier Style variant
Intent Style variant
<TimeInput />

01 Examples

Display format vs. bound value

format="12h" adds an AM/PM segment and withSeconds adds a seconds segment, but both change only what the field shows. The bound value stays a 24-hour string: 14:15 displays as 02:15 PM and still binds as 14:15.
Start

Value: 09:30

Meeting
AM

Value (24h): 14:15

Duration

Value: 13:45:30

<script>
  import { TimeInput } from '@urbicon-ui/blocks';
  let startTime = $state('09:30');
  let meetingTime = $state('14:15');
  let preciseTime = $state('13:45:30');
</script>
<TimeInput label="Start" bind:value={startTime} />
<TimeInput label="Meeting" format="12h" bind:value={meetingTime} />
<TimeInput label="Duration" withSeconds bind:value={preciseTime} />

Empty, half-typed, complete

The value is undefined while every segment is empty, null while the time is half-typed, and a string once it is complete, so an untouched optional field can pass while a half-typed one cannot. The hidden name input submits an empty string for both, so refuse null before the form submits. This field flags null when focus leaves it; onfocusout also fires on every hop between segments, so compare relatedTarget with the field first.
Pickup

Value: undefined

<script lang="ts">
  import { TimeInput } from '@urbicon-ui/blocks';
  let pickup = $state<string | null>();
  let left = $state(false);
  const error = $derived(left && pickup === null ? 'Finish the time or clear it' : undefined);
</script>
<TimeInput
  label="Pickup"
  {error}
  bind:value={pickup}
  onfocusout={(e) => {
    // Also fires when focus moves from the hour to the minutes.
    left = !e.currentTarget.contains(e.relatedTarget as Node | null);
  }}
/>

Range bounds

Type 06:00 and click away: values below min or above max clamp back into range on blur, and onValueChange fires with the corrected time. There is no out-of-range state: if 19:30 must be rejected rather than moved, leave max out and check the value yourself.
Appointment
Office hours

Value: 09:00

<script>
  let officeTime = $state('09:00');
</script>
<TimeInput
  label="Appointment"
  min="08:00"
  max="18:00"
  helper="Office hours"
  bind:value={officeTime}
/>

A 15-minute raster

step takes seconds, as on input type="time", and counts from min, or from midnight without one. The Arrow keys move the minutes 00, 15, 30, 45, and a typed 10:07 becomes 10:00 as soon as its minutes are complete, so the value never holds 10:07. A time above max clamps to the last raster point at or below it. An hourly step (3600) fixes the minutes, and the hour alone completes the time.
Slot

Value: 10:15

<script>
  let slot = $state('10:15');
</script>
<TimeInput label="Slot" step={900} bind:value={slot} />

02 Date + Time

TimeInput is the form family's time field: Calendar, DatePicker and DateRangePicker are for dates, TimeInput for the time of day. It edits only the time, so for a full timestamp pair it with a DatePicker as two separate fields. Each keeps its own value (a Date from the picker, an HH:MM string from the time field), and you join them yourself; toDateInputValue turns the Date into its local YYYY-MM-DD, as the example below does. What you get is a local wall-clock time, not a point in time: turning 2026-08-15T14:30 into an instant needs a time zone, and that decision stays with you.

Date and time in one row

The outer wrapper of each component takes the full width by default (TimeInput's fullWidth stretches only the box inside it), so a row needs w-auto on each; without it they stack at every width. flex-wrap then breaks the row when the container gets too narrow for both.
Time

startsAt: 2026-08-15T14:30

<script lang="ts">
  import { DatePicker, TimeInput, toDateInputValue } from '@urbicon-ui/blocks';

  let apptDate = $state<Date | undefined>(new Date(2026, 7, 15));
  let apptTime = $state('14:30');

  // A local wall-clock string. Give it a time zone before it becomes an instant.
  const startsAt = $derived(
    apptDate && apptTime ? `${toDateInputValue(apptDate)}T${apptTime}` : null
  );
</script>

<div class="flex flex-wrap items-end gap-3">
  <DatePicker label="Date" class="w-auto" bind:value={apptDate} />
  <TimeInput label="Time" class="w-auto" bind:value={apptTime} />
</div>

03 Customization

For a reusable look, register a named preset on <BlocksProvider>; for individual parts, use slotClasses. The slots are wrapper (what class also targets), label, requiredMark, field, icon (replace the clock with your own snippet via the icon prop), segment, separator, meridiem, and message. For a full ground-up restyle, set unstyled to drop every default class and rebuild from the slots.

showIcon={false} hides the leading clock icon, and fullWidth stretches the field to fill its container instead of hugging its content.

Boarding-pass segments via slotClasses

Each segment gets its own box, so the field reads as three fields rather than one.
Departure
<TimeInput
  label="Departure"
  bind:value={departure}
  slotClasses={{
    field: 'gap-1 border-transparent bg-transparent px-0',
    segment: 'rounded-modify border border-border-default bg-surface-subtle px-2 py-1 font-mono',
    separator: 'text-text-tertiary'
  }}
/>

04 Accessibility

Group semantics

The field is a role="group" named by its label (or aria-label).

Per-segment naming

Each segment (hour, minute, and, when present, second) carries its own aria-label.

Keyboard

Arrow Up / Down moves the focused segment by one and wraps inside it: 59 goes to 00 without carrying the hour. With step it moves to the segment's next raster value instead, and a segment the raster fixes is read-only and skipped. Arrow Left / Right moves between segments; typing digits auto-advances to the next. min and max do not limit stepping; they apply on blur.

The AM/PM segment

The AM/PM segment toggles by click, the Arrow keys, Enter / Space, or the A / P keys.

Errors are announced

The error message is announced via role="alert".

05 API Reference

29 props
29 props
Prop
Type
Default
Description

06 Types

2 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { TimeInput } from '@urbicon-ui/blocks';