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 && (
<FieldError
className='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 0023 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 Select inputs — consistent styling, full keyboard support
  • Field-context binding: reads value and validation state via useFieldContext
  • Range constraints: pass min / max (as HH:mm strings) 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 (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='space-y-4'
>
<form.AppField
name='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 (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='space-y-4'
>
<form.AppField
name='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.TimeField
label='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

On this page