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
<TimeInput />01 Examples
Display format vs. bound value
Value: 09:30
Value (24h): 14:15
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
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
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
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
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
<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 propsProp | Type | Default | Description | |
|---|---|---|---|---|
aria-label | string | — | Accessible name for the field group when no visible label is set. | |
class | string | — | Extra classes merged onto the root wrapper. | |
disabled | boolean | false | Blocks input and dims the whole field. | |
error | string | — | Error message below the field. When set it overrides helper, colours the
field danger, and marks the segments aria-invalid. | |
format | 12h24h | '24h' | Display the hour as 12-hour with an AM/PM segment. The value stays 24-hour. | |
fullWidth | boolean | false | Stretch the field to the full width of its container. | |
helper | string | — | Helper text below the field — hidden when error is present. | |
icon | Snippet | — | A custom leading icon; replaces the default clock. | |
id | string | — | Root id; the segments derive their ids and ARIA wiring from it. | |
intent variant | defaultsuccesswarningdanger | default | Controls the color theme and semantic meaning of the TimeInput. Affects the overall appearance and user perception. Available options: default, success, warning, danger. | |
label | string | — | Label text displayed above the field, linked via aria-labelledby. | |
max | string | — | Latest allowed time, canonical 24-hour HH:MM(:SS). Values above it are
clamped down on blur, to the last step raster point at or below it. | |
min | string | — | Earliest allowed time, canonical 24-hour HH:MM(:SS). Values below it are
clamped up on blur. It is also where the step raster starts. | |
name | string | — | Name for a hidden input carrying the canonical value, for native form
submission; it submits "" while the field is empty or half-typed. | |
onValueChange | (value: string | null | undefined) => void | — | Fires whenever the field changes the value (typing, the Arrow keys, Backspace,
the clamp on blur) with the canonical 24-hour string, null while half-typed,
or undefined once every segment is empty again. Setting value yourself does
not fire it. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ TimeInput: {...} }}>. | |
readonly | boolean | false | Shows the time but refuses edits; the segments stay focusable. | |
required | boolean | false | Marks the label with an asterisk and sets aria-required on the segments. It does
not block a native submit — the value lives in component state, so validate it
yourself before you act on it. | |
showIcon | boolean | true | Show the leading clock icon. | |
size variant | smmdlg | md | Controls the dimensions, padding, and text size of the TimeInput. Affects the component's physical footprint. Available options: sm, md, lg. | |
slotClasses | Partial<Record<TimeInputSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: wrapper (what
class also targets) | label | requiredMark | field | icon | segment |
separator | meridiem | message. | |
step | number | — | Raster in seconds, counted from min (or midnight) as on <input type="time">:
900 is a 15-minute grid, 3600 whole hours. Unset, every minute is allowed
(every second with withSeconds).
- From a time on the raster, the Arrow keys move a segment to its next raster
value and wrap inside it without carrying into the others. From a time off
it (one you passed in, or a digit mid-entry), Up goes to the first raster
point above and Down to the first below. aria-valuemin/aria-valuemax
name the first and last raster value of each segment for the time shown.
- A typed time off the raster snaps down to the raster point below it as soon
as its segment is complete. While a digit is still mid-entry and the time it
spells is off the raster, the value is null, so it never carries an
off-raster time and always matches what the field shows. A time you pass in is shown as given, fixed segments included,
and lands on the raster with the next edit or on blur.
- The raster counts from min: with min="09:10" and 900, a typed 10:00
lands on 09:55.
- A segment the raster pins to one value is shown but not typed: an hourly
step fixes the minute (and second), a whole-minute step fixes the second.
- Without withSeconds only the step's whole-minute points count: 30 allows
every minute, 90 every third.
- With a step that does not divide an hour (or a minute, with withSeconds),
typing and the blur clamp reach every raster point, but the Arrow keys only
move along points that keep the other segments; prefer a step that divides
the hour.
Must be a positive whole number; anything else is ignored, with a warning in dev. | |
tier variant | modifycommit | modify | Selects the semantic radius tier of the TimeInput — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: modify, commit. | |
unstyled | boolean | — | Remove all default tv() classes — only user-provided classes apply. | |
value | string | null | undefined | — | The time as the segments spell it, in one of three states:
- undefined while every segment is empty, untouched or cleared back to empty.
Leave the prop out (or bind undefined) for an empty field.
- null while the time is half-typed: at least one segment holds a digit and
at least one is blank, or, with a step, a mid-entry digit leaves the time
off the raster.
- A canonical 24-hour HH:MM / HH:MM:SS string once the time is complete;
format never changes it.
An AM/PM segment always holds a value, so it never makes a time half-typed.
"" reads as no time, like null and undefined. Passing no time clears the
field: one that shows a complete time clears wherever focus is, a half-typed
one only while focus is outside it. On a half-typed field with focus inside,
no time is taken as the echo of the field's own report and ignored, so a
consumer that stores no time as one value ("" or undefined) cannot wipe
the digits being typed; the cost is that a reset fired from inside a
half-typed field (an Escape handler on it, or a clear button that keeps
focus with a mousedown preventDefault) is ignored too, and nothing is
written back, so value and segments disagree until the next edit. A reset
has to change the value: a half-typed bound field already holds null, so
reset it with undefined. Where neither works (a reset from inside, or a
string-typed consumer that can only write ""), remount the field with
{#key}. A null you pass at mount renders empty and stays null until
the user edits. Supports bind:value. | |
variant variant | outlinedfilledghost | outlined | Controls the visual style and presentation of the TimeInput. Determines the component's visual treatment. Available options: outlined, filled, ghost. | |
withSeconds | boolean | false | Add a seconds segment. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'class' | 'id' | 'aria-label' | 'children') | |
...TimeInputVariants variant | VariantProps | — | Styling variants from TimeInputVariants |
06 Types
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
TimeInputProps | interface | props | — | — | |
TimeInputVariants | type | variant | — | — |
07 Installation
Import
import { TimeInput } from '@urbicon-ui/blocks';