A typed date of birth that cannot be in the future or implausibly old, and that shows the age it implies so a typo in the year is caught straight away. Use this when a form asks for a date of birth and a typo in the year should be obvious.
registry/patterns/birth-date-field/birth-date-field.tsx
"use client"
import * as React from "react"
import { DateField } from "@/components/ui/date-field"
import { ageOn, today as todayISO } from "@/components/ui/date-time-core"
export interface BirthDateFieldProps {
value?: string | null
defaultValue?: string | null
onValueChange?: (value: string | null) => void
maxAge?: number
today?: string
name?: string
locale?: string
}
export function validateBirthDate(
date: string | null,
today: string,
maxAge = 120
) {
if (!date) return { age: null, error: null }
if (date > today)
return { age: null, error: "A date of birth cannot be in the future." }
const age = ageOn(date, today)
if (age > maxAge)
return { age, error: `Check the year: that would make you ${age}.` }
return { age, error: null }
}
export function BirthDateField({
value,
defaultValue = null,
onValueChange,
maxAge = 120,
today,
name,
locale,
}: BirthDateFieldProps) {
const now = today ?? todayISO()
const [inner, setInner] = React.useState<string | null>(defaultValue)
const date = value === undefined ? inner : value
const { age, error } = validateBirthDate(date, now, maxAge)
const id = React.useId()
return (
<div className="flex flex-col gap-1.5">
<DateField
label="Date of birth"
locale={locale}
name={name}
value={date}
aria-describedby={id}
invalid={!!error}
onValueChange={(next) => {
if (value === undefined) setInner(next)
onValueChange?.(next)
}}
/>
<p
id={id}
role={error ? "alert" : "status"}
className={
error ? "text-destructive text-sm" : "text-muted-foreground text-sm"
}
>
{error ??
(age !== null
? `You are ${age} year${age === 1 ? "" : "s"} old.`
: "")}
</p>
</div>
)
}