Qure UI
Components

Time Field

A time, chosen from a stepped list or typed in whatever shorthand comes naturally.

09:30

'use client'import * as React from 'react'import { TimeField } from '@/registry/qure/ui/time-field'export default function TimeFieldDemo() {  const [value, setValue] = React.useState<string | null>('09:30')  return (    <div className="flex flex-col items-center gap-3">      <div className="w-32">        <TimeField value={value} onValueChange={setValue} aria-label="Time" />      </div>      <p className="text-label-base text-muted-foreground">        {value ?? 'Nothing set'}      </p>    </div>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/time-field.json

Usage

import { TimeField, parseTime } from "@/components/ui/time-field"
const [time, setTime] = React.useState<string | null>("09:30")

<TimeField value={time} onValueChange={setTime} aria-label="Time" />

The value is a "HH:mm" string in 24-hour form, or null. A string rather than a Date, because a time without a date is not a moment — attaching one silently invents a day.

Like Input, the field fills its container rather than sizing itself, so wrap it to set a width — w-32 fits HH:mm with the clock and clear affordances.

Typing is supported here

909:00
93009:30
9.3009:30
9:509:05
143014:30
9pm21:00
11:59 PM23:59
'use client'import * as React from 'react'import { TimeField, parseTime } from '@/registry/qure/ui/time-field'const SHORTHAND = ['9', '930', '9.30', '9:5', '1430', '9pm', '11:59 PM']export default function TimeFieldTyping() {  const [value, setValue] = React.useState<string | null>(null)  return (    <div className="flex flex-col items-center gap-4">      <div className="w-32">        <TimeField value={value} onValueChange={setValue} aria-label="Time" />      </div>      {/* The list steps by fifteen minutes; none of these are on it. All of          them are real times, so all of them are accepted. */}      <table className="text-label-base text-muted-foreground border-separate border-spacing-x-4">        <tbody>          {SHORTHAND.map((text) => (            <tr key={text}>              <td className="text-right font-mono">{text}</td>              <td className="text-foreground font-mono">{parseTime(text)}</td>            </tr>          ))}        </tbody>      </table>    </div>  )}

This is the opposite decision to DatePicker, and the difference is real rather than inconsistent. 03/04 is two different days depending on where the reader grew up; 14:30 is 14:30 everywhere. There is no ambiguity to get wrong — only shorthand to expand, and people type 9, 930, 9.30 and 9pm expecting all of them to work.

TypedRead as
9, 0909:00
930, 0930, 9.30, 9:3009:30
9:509:05
143014:30
9pm, 9 PM21:00
12am00:00

parseTime is exported, so the same rules can validate a form on submit rather than being reimplemented next to it.

The list is a convenience, not a constraint. 07:20 is a real time even when the list steps by fifteen minutes. A field that refuses it is a field people work around by writing the time into a notes box, where nothing can read it.

Something unreadable is left in the box rather than silently discarded — clearing what someone typed is how a field loses an appointment time without ever saying which one. The field marks itself invalid instead.

Stepping and bounds

<TimeField step={30} min="08:00" max="18:00" />

step sets the spacing of the suggestions, min and max trim them. None of the three restrict what can be typed — they shape the list, which is the part that is a convenience.

With a date

8/14/2026, 2:00:00 PM

'use client'import * as React from 'react'import { DatePicker } from '@/registry/qure/ui/date-picker'import { Label } from '@/registry/qure/ui/label'import { TimeField } from '@/registry/qure/ui/time-field'export default function TimeFieldDatetime() {  const [date, setDate] = React.useState<Date | null>(new Date())  const [time, setTime] = React.useState<string | null>('14:00')  /* Two controls, one value. Kept separate because a combined datetime picker     makes changing only the time mean re-choosing the date. */  const when = React.useMemo(() => {    if (!date || !time) return null    const [h, m] = time.split(':').map(Number)    return new Date(date.getFullYear(), date.getMonth(), date.getDate(), h, m)  }, [date, time])  return (    <div className="flex flex-col gap-2">      <Label>Appointment</Label>      <div className="flex items-start gap-2">        <div className="w-56">          <DatePicker            size="sm"            value={date}            onValueChange={(v) => setDate(v as Date | null)}            min={new Date()}          />        </div>        <div className="w-32">          {/* Same size as the DatePicker beside it. Two controls on one row              at different heights is the most visible way to get a form              wrong, and the sizes do not default to matching. */}          <TimeField            size="sm"            value={time}            onValueChange={setTime}            step={30}            min="08:00"            max="18:00"            aria-label="Appointment time"          />        </div>      </div>      <p className="text-label-base text-muted-foreground">        {when ? when.toLocaleString() : 'Incomplete'}      </p>    </div>  )}

Two controls rather than one. A combined datetime picker makes changing just the time mean re-choosing the date, which is the more common of the two edits.

Twelve-hour display

<TimeField hour12 />

Only the suggestions change. The value stays 24-hour, so nothing downstream has to know how it was displayed.

API Reference

Prop

Type

On this page