A date of birth that decides whether someone is old enough, with the exact date they become eligible, without ever blocking typing. Use this when a product or purchase has a minimum age and the person should learn when they will qualify.
registry/patterns/age-gate-date-field/age-gate-date-field.tsx
"use client"
import * as React from "react"
import { DateField } from "@/components/ui/date-field"
import {
addYears,
formatDate,
resolveLocale,
today as todayISO,
} from "@/components/ui/date-time-core"
export interface AgeGateDateFieldProps {
minAge?: number
onValueChange?: (date: string | null, eligible: boolean) => void
today?: string
locale?: string
}
/** The day a person born on `birth` turns `minAge` (29 February becomes 1 March in other years). */
export function eligibleOn(birth: string, minAge: number) {
const d = addYears(birth, minAge)
return birth.slice(5) === "02-29" && d.slice(5) === "02-28"
? `${d.slice(0, 4)}-03-01`
: d
}
export function AgeGateDateField({
minAge = 18,
onValueChange,
today,
locale,
}: AgeGateDateFieldProps) {
const loc = resolveLocale(locale)
const now = today ?? todayISO()
const [date, setDate] = React.useState<string | null>(null)
const from = date ? eligibleOn(date, minAge) : null
const eligible = !!from && from <= now
return (
<div className="flex flex-col gap-1.5">
<DateField
label="Date of birth"
locale={loc}
value={date}
invalid={!!date && !eligible}
onValueChange={(next) => {
setDate(next)
onValueChange?.(next, !!next && eligibleOn(next, minAge) <= now)
}}
/>
<p
role="status"
className={
date && !eligible
? "text-destructive text-sm"
: "text-muted-foreground text-sm"
}
>
{!from
? `You must be ${minAge} or older.`
: eligible
? "Thanks, you can continue."
: `You can sign up from ${formatDate(from, loc, { dateStyle: "medium" })}.`}
</p>
</div>
)
}