Memorable date
StableDay, month and year as three plain fields (the GOV.UK date input) for birthdays and dates people know by heart.
Nothing jumps to the next field on its own, so people can check what they typed. The month takes names (“mar”, “March”) as well as numbers, and the fields follow the locale’s order. validateMemorableDate returns one error at a time, with the fields it is about: something missing, then a day or month it can’t read, a year without 4 digits, a date that doesn’t exist (31 Feb), and last past or future.
Date of birth
import { useState, type FormEvent } from 'react'import { MemorableDateInput, validateMemorableDate, type MemorableDate } from '@rchursin/product/calendar'import { Button } from '@rchursin/product/ui'
/** * A date people know by heart, as three plain fields. Nothing jumps ahead while you type, and * the month takes a name ("mar", "March") as well as a number. Checked when you continue. */export default function MemorableDateBasic() { const [dob, setDob] = useState<MemorableDate>({ day: '', month: '', year: '' }) const [submitted, setSubmitted] = useState(false) const check = validateMemorableDate(dob, { label: 'Date of birth', past: true })
function submit(e: FormEvent) { e.preventDefault() setSubmitted(true) }
return ( <form onSubmit={submit} noValidate className="flex w-full max-w-80 flex-col gap-5"> <MemorableDateInput legend="What is your date of birth?" value={dob} onChange={setDob} error={submitted ? check.error : undefined} errorFields={check.fields} autoComplete="bday" name="dob" /> <Button type="submit" className="self-start"> Continue </Button> <p role="status" className="-mt-2 min-h-4 font-mono text-[11px] text-fg-3"> {submitted && check.iso && `Reads as ${check.iso}`} </p> </form> )}Usage
import { MemorableDateInput, validateMemorableDate, type MemorableDate } from '@rchursin/product/calendar'const [dob, setDob] = useState<MemorableDate>({ day: '', month: '', year: '' })const check = validateMemorableDate(dob, { label: 'Date of birth', past: true })
<MemorableDateInput legend="What is your date of birth?" value={dob} onChange={setDob} error={submitted ? check.error : undefined} errorFields={check.fields} autoComplete="bday" />Examples
One error at a time
Starts on 31 Feb with a two-digit year: the year is reported first. Make it 2026 and “must be a real date” takes over, on the day only. Only the fields an error names get the firmer edge.
import { useState } from 'react'import { MemorableDateInput, validateMemorableDate, type MemorableDate } from '@rchursin/product/calendar'
/** * One error at a time, in a fixed order: missing → a day or month it can't read → a year without * 4 digits → a date that doesn't exist → past or future. Only the fields an error names get the * firmer edge. Make the year 2026 and the next problem shows. */export default function MemorableDateErrors() { const [started, setStarted] = useState<MemorableDate>({ day: '31', month: 'feb', year: '26' }) const check = validateMemorableDate(started, { label: 'Start date', past: true })
return ( <div className="flex w-full max-w-80 flex-col gap-4"> <MemorableDateInput legend="When did you first run this agent?" value={started} onChange={setStarted} error={check.error} errorFields={check.fields} /> <p className="font-mono text-[11px] text-fg-3">{check.iso ? `iso: "${check.iso}"` : `fields: ${check.fields?.join(', ')}`}</p> </div> )}API
MemorableDateInput props
| Prop | Type | Default | Description |
|---|---|---|---|
legendrequired | string | – | The question, as the fieldset’s legend: “What is your date of birth?”. |
hint | string | – | Defaults to an example in the locale’s order, e.g. “For example, 3 27 2007” in the US. |
valuerequired | MemorableDate | – | The three fields as typed: { day, month, year }, all strings. |
onChangerequired | (value: MemorableDate) => void | – | Called on every keystroke with all three fields. |
error | string | – | The one error to show, under the hint. |
errorFields | DatePart[] | – | The fields the error is about; they get aria-invalid and a firmer edge. All three when left out. |
autoComplete | 'bday' | – | Fills from the browser’s saved birthday (bday-day, bday-month, bday-year), as WCAG 1.3.5 asks. |
name | string | – | With name="dob", the fields are named dob-day, dob-month and dob-year. |
className | string | – | Classes on the fieldset. |
validateMemorableDate(value, options)
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | MemorableDate | – | What was typed. |
options.label | string | 'Date' | Starts each message: “Date of birth must be a real date”. |
options.past | boolean | false | The date must be before today. |
options.future | boolean | false | The date must be after today. |
returns | MemorableDateCheck | – | { iso, error?, fields? }: the ISO date when everything is valid (else null), the one message to show, and the parts it is about. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| TabShiftTab | Moves between Day, Month and Year (in the locale’s order). Nothing moves on by itself. |
- A fieldset with the question as its legend; the hint and the error are linked to it with aria-describedby.
- Each field has its own visible label (Day, Month, Year).
- Errors start with a hidden “Error:” and an alert icon, so they never rely on colour.
- Day and year ask for the number pad; month uses the text keyboard so names can be typed.
Guidelines
Do
- Use it for dates people remember: birthdays, when something started.
- Show the error only after a submit, then keep it in step as they type.
- Set autoComplete="bday" for a date of birth.
Don’t
- Don’t add a calendar to it; scrolling back 30 years is slower than typing.
- Don’t move focus between the fields automatically.
In use
Related
- Date fieldA date typed in segments in the locale’s order, with a calendar button beside it. Paste "2026-10-21" to fill it.
- Text inputA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
- Error summaryA “There is a problem” box at the top of a form that takes focus after a failed submit and links each message to its field.
- Form toolkituseForm for values, errors and validation timing, plus the checks and GOV.UK-style messages every form in the kit shares.
Source
The component’s own files, exactly as they run on this site.
/** * MemorableDateInput: day, month and year as three plain fields (the GOV.UK date input), for * dates people know without looking: a birthday, the day something started. * * const [dob, setDob] = useState<MemorableDate>({ day: '', month: '', year: '' }) * const check = validateMemorableDate(dob, { label: 'Date of birth', past: true }) * <MemorableDateInput legend="What is your date of birth?" value={dob} onChange={setDob} * error={submitted ? check.error : undefined} errorFields={check.fields} autoComplete="bday" /> * * Rules it follows: never jumps to the next field by itself (people check what they typed); * accepts month names ("mar", "March") as well as numbers, which cuts errors and helps people * with dyscalculia; fields follow the locale's order; one error at a time, in the order * missing → can't be read (day not a number, month unknown) → year not 4 digits → not a real date * (31 Feb) → breaks a rule ("must be in the past"). */import { useId, useMemo } from "react"import { cx } from "../../components/cx"import { ErrorText, TextInput } from "../ui/fields"import { dateFieldOrder,