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>
<Button
type='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'>
<Calendar
mode='single'
selected={value}
onSelect={(date) => {
field.handleChange(date);
setOpen(false);
}}
disabled={disabledMatchers.length > 0 ? disabledMatchers : undefined}
locale={locale}
autoFocus
/>
</PopoverContent>
</Popover>
{!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...

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 any date-fns/locale via the locale prop
  • Bounded selection: minDate / maxDate disable days outside the range in the calendar UI
  • Accessible trigger: a real <Button> with aria-invalid reflecting !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

Loading...
'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 (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='space-y-4'
>
<form.AppField
name='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

Loading...
'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 (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='space-y-4'
>
<form.AppField
name='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.DateField
label='Appointment'
description='Within the next 12 months'
minDate={today}
maxDate={oneYearFromNow}
/>
)}
/>
<form.AppForm>
<form.SubmitButton>Book</form.SubmitButton>
</form.AppForm>
</form>
);
}

Props

Prop

Type

On this page