Time Field
Time picker (HH:mm) integrated with TanStack Form via field context. Two shadcn Select inputs for hours and minutes.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-time-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-time-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-time-field.json
'use client';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';import { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';import { HOUR_OPTIONS, isHourDisabled, isMinuteDisabled, joinTime, MINUTE_OPTIONS, splitTime } from '@/lib/time';export interface TimeFieldProps {label?: string;description?: string;min?: string;max?: string;disabled?: boolean;fieldProps?: React.ComponentProps<typeof Field>;}export function TimeField({ label, description, min, max, disabled, fieldProps }: TimeFieldProps) {const field = useFieldContext<string>();const { isValid, errors } = field.state.meta;const { hour, minute } = splitTime(field.state.value ?? '');const bounds = { min, max };return (<Field className='gap-2' data-invalid={!isValid} {...fieldProps}>{label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}<div className='flex items-center gap-2'><Select value={hour} onValueChange={(h) => field.handleChange(joinTime(h, minute))} disabled={disabled}><SelectTrigger id={field.name} aria-invalid={!isValid} className='w-full'><SelectValue placeholder='HH' /></SelectTrigger><SelectContent>{HOUR_OPTIONS.map((h) => (<SelectItem key={h} value={h} disabled={isHourDisabled(h, bounds)}>{h}</SelectItem>))}</SelectContent></Select><span className='text-muted-foreground'>:</span><Select value={minute} onValueChange={(m) => field.handleChange(joinTime(hour, m))} disabled={disabled}><SelectTrigger aria-invalid={!isValid} className='w-full'><SelectValue placeholder='mm' /></SelectTrigger><SelectContent>{MINUTE_OPTIONS.map((m) => (<SelectItem key={m} value={m} disabled={isMinuteDisabled(m, hour, bounds)}>{m}</SelectItem>))}</SelectContent></Select></div>{!isValid && (<FieldErrorclassName='text-xs text-left'errors={errors.map((error) => ({ message: typeof error === 'string' ? error : error?.message }))}/>)}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
Loading...
TimeField is a time-only picker built from two shadcn/ui Select inputs — hours 00–23 and minutes in 5-minute steps. It binds to a string in the HH:mm 24-hour format and reads its state from TanStack Form's field context.
Built-in features
- Two-select picker: hours and minutes as shadcn
Selectinputs — consistent styling, full keyboard support - Field-context binding: reads value and validation state via
useFieldContext - Range constraints: pass
min/max(asHH:mmstrings) to disable out-of-range options - Validation: surfaces TanStack Form validator errors below the picker
Setup
Register the field component on your form hook, then render it inside form.AppField:
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { TimeField } from '@/components/ui/shuip/tanstack-form/time-field';
const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TimeField },
formComponents: {},
});
<form.AppField
name='meetingTime'
children={(field) => <field.TimeField label='Meeting time' />}
/>The value is a string in HH:mm format (or empty string when unset). Picking only the hour or only the minute defaults the other part to 00.
Constraining the range
min and max accept HH:mm strings; out-of-range hour and minute options are disabled:
<field.TimeField label='Appointment' min='09:00' max='18:00' />Examples
Default
Loading...
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';import { TimeField } from '@/components/ui/shuip/tanstack-form/time-field';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { TimeField },formComponents: { SubmitButton },});export default function TsfTimeFieldExample() {const form = useAppForm({defaultValues: {meetingTime: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 500));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><form.AppFieldname='meetingTime'validators={{onChange: ({ value }) => (!value ? 'Time is required' : undefined),}}children={(field) => <field.TimeField label='Meeting time' description='Pick a time' />}/><form.AppForm><form.SubmitButton>Save</form.SubmitButton></form.AppForm></form>);}
Validation
Loading...
'use client';import { createFormHook } from '@tanstack/react-form';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';import { TimeField } from '@/components/ui/shuip/tanstack-form/time-field';const BUSINESS_OPEN = '09:00';const BUSINESS_CLOSE = '18:00';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { TimeField },formComponents: { SubmitButton },});export default function TsfTimeFieldValidationExample() {const form = useAppForm({defaultValues: {appointment: '',},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 500));alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><form.AppFieldname='appointment'validators={{onChange: ({ value }) => {if (!value) return 'Time is required';if (value < BUSINESS_OPEN || value > BUSINESS_CLOSE) {return `Appointment must be between ${BUSINESS_OPEN} and ${BUSINESS_CLOSE}`;}return undefined;},}}children={(field) => (<field.TimeFieldlabel='Appointment'description={`Office hours: ${BUSINESS_OPEN} – ${BUSINESS_CLOSE}`}min={BUSINESS_OPEN}max={BUSINESS_CLOSE}/>)}/><form.AppForm><form.SubmitButton>Book</form.SubmitButton></form.AppForm></form>);}
Props
Prop
Type