Date Field
Single-date picker integrated with TanStack Form via React context. Wraps the shadcn Calendar inside a Popover triggered by an outline Button.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-date-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-date-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-date-field.json
'use client';import { format } from 'date-fns';import type { Locale } from 'date-fns/locale';import { enUS } from 'date-fns/locale';import { CalendarIcon } from 'lucide-react';import * as React from 'react';import type { Matcher } from 'react-day-picker';import { Button } from '@/components/ui/button';import { Calendar } from '@/components/ui/calendar';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';import { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';import { cn } from '@/lib/utils';export interface DateFieldProps {label?: string;description?: string;placeholder?: string;minDate?: Date;maxDate?: Date;locale?: Locale;dateFormat?: string;disabled?: boolean;fieldProps?: React.ComponentProps<typeof Field>;triggerProps?: React.ComponentProps<typeof Button>;}export function DateField({label,description,placeholder = 'Pick a date',minDate,maxDate,locale = enUS,dateFormat = 'PPP',disabled,fieldProps,triggerProps,}: DateFieldProps) {const field = useFieldContext<Date | undefined>();const { isValid, errors } = field.state.meta;const [open, setOpen] = React.useState(false);const value = field.state.value;const disabledMatchers: Matcher[] = [];if (minDate) disabledMatchers.push({ before: minDate });if (maxDate) disabledMatchers.push({ after: maxDate });return (<Field className='gap-2' data-invalid={!isValid} {...fieldProps}>{label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}<Popover open={open} onOpenChange={setOpen}><PopoverTrigger asChild><Buttontype='button'id={field.name}variant='outline'disabled={disabled}aria-invalid={!isValid}onBlur={field.handleBlur}{...triggerProps}className={cn('w-full justify-between font-normal',!value && 'text-muted-foreground',triggerProps?.className,)}><span>{value ? format(value, dateFormat, { locale }) : placeholder}</span><CalendarIcon className='size-4 opacity-50' /></Button></PopoverTrigger><PopoverContent className='w-auto p-0' align='start'><Calendarmode='single'selected={value}onSelect={(date) => {field.handleChange(date);setOpen(false);}}disabled={disabledMatchers.length > 0 ? disabledMatchers : undefined}locale={locale}autoFocus/></PopoverContent></Popover>{!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>);}
DateField is a single-date picker that encapsulates TanStack Form's field management with the shadcn Calendar primitive. The trigger is an outline Button (full-width) that shows the selected date formatted via date-fns; clicking it opens a Popover containing the calendar.
It reads the surrounding field via useFieldContext<Date | undefined>(), so you compose it inside a <form.AppField> rather than passing a form instance down. undefined represents an empty selection (matching react-day-picker's convention).
Built-in features
- Context-bound field state: reads the field via
useFieldContext— no prop drilling - Locale-aware formatting: defaults to
en-US; pass anydate-fns/localevia thelocaleprop - Bounded selection:
minDate/maxDatedisable days outside the range in the calendar UI - Accessible trigger: a real
<Button>witharia-invalidreflecting!isValid - Validator-friendly: works with TanStack Form's native validators or any standard schema
Setup
Field components are bound via React context. In your project, create lib/form.ts once:
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { DateField } from '@/components/ui/shuip/tanstack-form/date-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { DateField },
formComponents: { SubmitButton },
});See the form-context item for details.
Then compose the field inside <form.AppField>:
import { useAppForm } from '@/lib/form';
const form = useAppForm({
defaultValues: { dueDate: undefined as Date | undefined },
onSubmit: async ({ value }) => saveData(value),
});
<form.AppField
name='dueDate'
validators={{
onChange: ({ value }) => (value ? undefined : 'Pick a due date'),
}}
children={(field) => <field.DateField label='Due date' />}
/>Bounded selection
Use minDate and maxDate to disable out-of-range days directly in the calendar:
<form.AppField
name='appointment'
children={(field) => (
<field.DateField label='Appointment' minDate={new Date()} maxDate={oneYearFromNow} />
)}
/>Disabled days remain visible but cannot be selected. Mirror the bounds in your validator so submission also rejects out-of-range values.
Examples
Default
'use client';import { createFormHook } from '@tanstack/react-form';import { DateField } from '@/components/ui/shuip/tanstack-form/date-field';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { DateField },formComponents: { SubmitButton },});export default function TsfDateFieldExample() {const form = useAppForm({defaultValues: {dueDate: undefined as Date | undefined,},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 500));alert(JSON.stringify({ dueDate: value.dueDate?.toISOString() ?? null }, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><form.AppFieldname='dueDate'validators={{onChange: ({ value }) => (value ? undefined : 'Pick a due date'),}}children={(field) => <field.DateField label='Due date' description='When should this task be done?' />}/><form.AppForm><form.SubmitButton>Save</form.SubmitButton></form.AppForm></form>);}
Validation
'use client';import { createFormHook } from '@tanstack/react-form';import { DateField } from '@/components/ui/shuip/tanstack-form/date-field';import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';const today = new Date();today.setHours(0, 0, 0, 0);const oneYearFromNow = new Date(today);oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { DateField },formComponents: { SubmitButton },});export default function TsfDateFieldValidationExample() {const form = useAppForm({defaultValues: {appointment: undefined as Date | undefined,},onSubmit: async ({ value }) => {await new Promise((resolve) => setTimeout(resolve, 500));alert(`Appointment: ${value.appointment?.toLocaleDateString() ?? 'none'}`);},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='space-y-4'><form.AppFieldname='appointment'validators={{onChange: ({ value }) => {if (!value) return 'Pick an appointment date';if (value < today) return 'Appointment must be in the future';if (value > oneYearFromNow) return 'Appointment must be within a year';return undefined;},}}children={(field) => (<field.DateFieldlabel='Appointment'description='Within the next 12 months'minDate={today}maxDate={oneYearFromNow}/>)}/><form.AppForm><form.SubmitButton>Book</form.SubmitButton></form.AppForm></form>);}
Props
Prop
Type