Date Field
Single-date picker integrated with React Hook Form via typed lens binding from @hookform/lenses. Wraps the shadcn Calendar inside a Popover triggered by an outline Button.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-date-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-date-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-date-field.json
'use client';import type { Lens } from '@hookform/lenses';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 { useController } from 'react-hook-form';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 { cn } from '@/lib/utils';export interface DateFieldProps {lens: Lens<Date | undefined>;label?: string;description?: string;placeholder?: string;minDate?: Date;maxDate?: Date;locale?: Locale;disabled?: boolean;dateFormat?: string;triggerProps?: React.ComponentProps<typeof Button>;}export function DateField({lens,label,description,placeholder = 'Pick a date',minDate,maxDate,locale = enUS,disabled,dateFormat = 'PPP',triggerProps,}: DateFieldProps) {const { field, fieldState } = useController(lens.interop());const [open, setOpen] = React.useState(false);const value = field.value as Date | undefined;const disabledMatchers: Matcher[] = [];if (minDate) disabledMatchers.push({ before: minDate });if (maxDate) disabledMatchers.push({ after: maxDate });return (<Field className='gap-2' data-invalid={fieldState.invalid}>{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={fieldState.invalid}onBlur={field.onBlur}{...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.onChange(date);setOpen(false);}}disabled={disabledMatchers.length > 0 ? disabledMatchers : undefined}locale={locale}autoFocus/></PopoverContent></Popover>{fieldState.invalid && <FieldError className='text-xs text-left' errors={[fieldState.error]} />}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
DateField is a single-date picker that encapsulates React Hook 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.
The field binds to the form via a typed lens from @hookform/lenses. The value type is Date | undefined — undefined represents an empty selection (matching react-day-picker's convention).
Built-in features
- Typed lens binding:
lens.focus('dueDate')—Lens<Date | undefined>is enforced at compile time - 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-invalidreflectingfieldState.invalid - Zod validation: integrates with
z.date().min(...).max(...)via the standard resolver
Setup
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { DateField } from '@/components/ui/shuip/react-hook-form/date-field';
const form = useForm<{ dueDate: Date | undefined }>({
defaultValues: { dueDate: undefined },
});
const lens = useLens({ control: form.control });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DateField lens={lens.focus('dueDate')} label='Due date' />
</form>
</Form>The <Form> wrapper is required — it provides React Hook Form's FormProvider context.
Bounded selection
Use minDate and maxDate to disable out-of-range days directly in the calendar:
<DateField
lens={lens.focus('appointment')}
label='Appointment'
minDate={new Date()}
maxDate={oneYearFromNow}
/>Disabled days remain visible but cannot be selected. Validate the same bounds in your schema so submission also rejects out-of-range values.
Examples
Default
'use client';import { useLens } from '@hookform/lenses';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { DateField } from '@/components/ui/shuip/react-hook-form/date-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({dueDate: z.date({ message: 'Pick a due date' }),});type Values = z.infer<typeof zodSchema>;export default function RhfDateFieldExample() {const form = useForm<Values>({defaultValues: { dueDate: undefined as unknown as Date },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {alert(`Due date: ${values.dueDate.toISOString()}`);}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><DateField lens={lens.focus('dueDate')} label='Due date' description='When should this task be done?' /><SubmitButton>Save</SubmitButton></form></Form>);}
Validation
'use client';import { useLens } from '@hookform/lenses';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { DateField } from '@/components/ui/shuip/react-hook-form/date-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const today = new Date();today.setHours(0, 0, 0, 0);const oneYearFromNow = new Date(today);oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);const zodSchema = z.object({appointment: z.date({ message: 'Pick an appointment date' }).min(today, 'Appointment must be in the future').max(oneYearFromNow, 'Appointment must be within a year'),});type Values = z.infer<typeof zodSchema>;export default function RhfDateFieldValidationExample() {const form = useForm<Values>({defaultValues: { appointment: undefined as unknown as Date },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {alert(`Appointment: ${values.appointment.toLocaleDateString()}`);}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><DateFieldlens={lens.focus('appointment')}label='Appointment'description='Within the next 12 months'minDate={today}maxDate={oneYearFromNow}/><SubmitButton>Book</SubmitButton></form></Form>);}
Props
Prop
Type