react.form.date-input
Day, month and year boxes under one legend, with hint, error message and the boxes in error marked (GOV.UK date input).
React ^19 · TypeScript only A React component: use it from a TypeScript project with React installed. A client component ("use client").
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript.
What it does
A date asked as three text boxes - Day, Month, Year - under one `<legend>`, with a hint and an error message described to the whole group. It is the GOV.UK Design System date input as a React component, and it is made to take `form.date-parts`'s answer directly: its `message` is the `error`, its `fields` the `errorFields`, so exactly the boxes that need fixing are marked.
"use client";
import { useState } from "react";
import { DateInput } from "#fune/react.form.date-input@^1";
import { validateDateParts, type DateParts } from "#fune/form.date-parts@^1";
const [dob, setDob] = useState<DateParts>({ day: "", month: "", year: "" });
const check = submitted
? validateDateParts(dob, "Date of birth", { today, timing: "past", notBefore: null, notAfter: null })
: null;
<DateInput id="dob" legend="What is your date of birth?" hint="For example, 27 3 2007" birthday
value={dob} onChange={setDob}
error={check?.message} errorFields={check?.fields} />
For example
-
three boxes under a legend, named after the id
<DateInput id="passport-issued" legend="When was your passport issued?" />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">When was your passport issued?</legend><div class="fune-date-input" id="passport-issued"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-day">Day</label><input class="fune-input fune-input--width-2" id="passport-issued-day" type="text" inputMode="numeric" name="passport-issued-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-month">Month</label><input class="fune-input fune-input--width-2" id="passport-issued-month" type="text" inputMode="numeric" name="passport-issued-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-year">Year</label><input class="fune-input fune-input--width-4" id="passport-issued-year" type="text" inputMode="numeric" name="passport-issued-year"/></div></div></div></fieldset></div> -
a hint describes the group, GOV.UK's example
<DateInput id="passport-issued" legend="When was your passport issued?" hint="For example, 27 3 2007" isPageHeading />renders
<div class="fune-field"><fieldset class="fune-fieldset" aria-describedby="passport-issued-hint"><legend class="fune-legend fune-legend--heading"><h1 class="fune-legend-heading">When was your passport issued?</h1></legend><div class="fune-hint" id="passport-issued-hint">For example, 27 3 2007</div><div class="fune-date-input" id="passport-issued"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-day">Day</label><input class="fune-input fune-input--width-2" id="passport-issued-day" type="text" inputMode="numeric" name="passport-issued-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-month">Month</label><input class="fune-input fune-input--width-2" id="passport-issued-month" type="text" inputMode="numeric" name="passport-issued-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-year">Year</label><input class="fune-input fune-input--width-4" id="passport-issued-year" type="text" inputMode="numeric" name="passport-issued-year"/></div></div></div></fieldset></div>
The function
Written for React ^19, in TypeScript only. A component: it takes its props and renders HTML, which the tests below pin exactly.
export function DateInput(props: DateInputProps): JSX.Element
Its props, DateInputProps. A ? marks one the caller may leave out.
| legend | ReactNode | the question, such as "What is your date of birth?" |
| id? | string | the date wrapper's id, and the start of the hint's and error's; the boxes are <id>-day, <id>-month and <id>-year; React's useId() when left out |
| name? | string | the start of the boxes' names, <name>-day and so on; the id when left out |
| hint? | ReactNode | help under the legend, such as "For example, 27 3 2007" |
| error? | ReactNode | what is wrong with the date, such as validateDateParts's message |
| errorFields? | readonly string[] | which of "day", "month" and "year" to mark in error (validateDateParts's fields); all three when left out or empty |
| value? | DateParts | the three boxes' text, for a controlled input (with onChange) |
| defaultValue? | DateParts | the starting text, for an uncontrolled one |
| onChange? | (value: DateParts) => void | called with all three boxes' text whenever one changes |
| onBlur? | () => void | called when any of the boxes loses focus, to mark the field touched |
| birthday? | boolean | a date of birth: the boxes autocomplete as bday-day, bday-month and bday-year |
| isPageHeading? | boolean | the legend is the page's heading |
| disabled? | boolean | shown but not editable, and not submitted |
| className? | string | added to the wrapper's classes |
| renders | JSX.Element |
The type it declares, generated into your project
/** One date asked as three boxes: day, month and year. */
export interface DateInputProps {
/** the question, such as "What is your date of birth?" */
readonly legend: ReactNode;
/** the date wrapper's id, and the start of the hint's and error's; the boxes are <id>-day, <id>-month and <id>-year; React's useId() when left out */
readonly id?: string;
/** the start of the boxes' names, <name>-day and so on; the id when left out */
readonly name?: string;
/** help under the legend, such as "For example, 27 3 2007" */
readonly hint?: ReactNode;
/** what is wrong with the date, such as validateDateParts's message */
readonly error?: ReactNode;
/** which of "day", "month" and "year" to mark in error (validateDateParts's fields); all three when left out or empty */
readonly errorFields?: readonly string[];
/** the three boxes' text, for a controlled input (with onChange) */
readonly value?: DateParts;
/** the starting text, for an uncontrolled one */
readonly defaultValue?: DateParts;
/** called with all three boxes' text whenever one changes */
readonly onChange?: (value: DateParts) => void;
/** called when any of the boxes loses focus, to mark the field touched */
readonly onBlur?: () => void;
/** a date of birth: the boxes autocomplete as bday-day, bday-month and bday-year */
readonly birthday?: boolean;
/** the legend is the page's heading */
readonly isPageHeading?: boolean;
/** shown but not editable, and not submitted */
readonly disabled?: boolean;
/** added to the wrapper's classes */
readonly className?: string;
}
Your code names it in one line, in the file that uses it
import { DateInput } from "#fune/react.form.date-input@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
"use client";
import { useId, useState, type JSX, type ReactNode } from "react";
import type { DateParts } from "./form_date_parts.ts"; ← from form.date-parts ^1.0.0 · built alongside by fune
import { Fieldset } from "./react_form_fieldset.ts"; ← from react.form.fieldset ^1.0.0 · built alongside by fune
import { FormField } from "./react_form_form_field.ts"; ← from react.form.form-field ^1.0.0 · built alongside by fune
import type { DateInputProps } from "./react_form_date_input_types.ts";
type Part = "day" | "month" | "year";
const BOXES: readonly { part: Part; label: string; width: string; autoComplete: string }[] = [
{ part: "day", label: "Day", width: "fune-input--width-2", autoComplete: "bday-day" },
{ part: "month", label: "Month", width: "fune-input--width-2", autoComplete: "bday-month" },
{ part: "year", label: "Year", width: "fune-input--width-4", autoComplete: "bday-year" },
];
function present(node: ReactNode): boolean {
return node !== null && node !== undefined && node !== false && node !== "";
}
/**
* The GOV.UK date input: a fieldset whose legend asks for the date, the hint
* and error message described to the whole group, and three text boxes with
* a numeric keyboard (not type="number", which GOV.UK advises against). With
* an error, the boxes named in errorFields are marked, or all three when it
* names none, as GOV.UK does for an error about the whole date.
*/
export function DateInput(props: DateInputProps): JSX.Element {
const auto = useId();
const { legend, hint, error, errorFields, value, defaultValue, onChange, onBlur, birthday, isPageHeading, disabled, className } = props;
const id = props.id ?? auto;
const name = props.name ?? id;
// The boxes' text as it stands, so onChange can hand over the whole date
// even when the input is uncontrolled.
const [own, setOwn] = useState<DateParts>(value ?? defaultValue ?? { day: "", month: "", year: "" });
const current = value ?? own;
const inError = present(error);
const marked = (part: Part) => inError && (!errorFields || errorFields.length === 0 || errorFields.includes(part));
function change(part: Part, text: string): void {
const next: DateParts = { ...current, [part]: text };
setOwn(next);
onChange?.(next);
}
return (
<Fieldset id={id} legend={legend} hint={hint} error={error} isPageHeading={isPageHeading} className={className}>
<div className="fune-date-input" id={id}>
{BOXES.map(({ part, label, width, autoComplete }) => (
<div className="fune-date-input-item" key={part}>
<FormField id={`${id}-${part}`} label={label}>
<input
className={["fune-input", marked(part) ? "fune-input--error" : null, width].filter(Boolean).join(" ")}
id={`${id}-${part}`}
name={`${name}-${part}`}
type="text"
inputMode="numeric"
autoComplete={birthday ? autoComplete : undefined}
disabled={disabled}
aria-invalid={marked(part) ? true : undefined}
value={value ? value[part] : undefined}
defaultValue={value ? undefined : defaultValue?.[part]}
onChange={(event) => change(part, event.target.value)}
onBlur={onBlur ? () => onBlur() : undefined}
/>
</FormField>
</div>
))}
</div>
</Fieldset>
);
}Install
npm install react react-dom
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project) with React ^19 installed (Functional Weave does not ship it, and fune build stops with the npm install line if it is missing), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add react.form.date-input
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.date-input-1.0.0-typescript.fune, or fetch it from a terminal with fune pull react.form.date-input@1.0.0:typescript.
It is React ^19 · TypeScript only, so there is no package for Python or Rust. The full package is one file too: react.form.date-input-1.0.0.fune, 30,067 bytes, sha256 4ab4b00c1d5f4be0f41206c09a8fa863929f96c5158798f99a0ba0338d83d027.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before react.form.date-input
after — your function gets the result and the arguments, and returns the final result.
// fune: after react.form.date-input
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace form.date-parts in react.form.date-input
// fune: replace react.form.fieldset in react.form.date-input
// fune: replace react.form.form-field in react.form.date-input
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show react.form.date-input --steps.
// fune: step react.form.date-input after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript alone, with the React in tooling/react, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json. A component’s tests are render tests: the props go in, and the HTML renderToStaticMarkup makes of them has to match exactly; a React warning during the render fails the test.
-
three boxes under a legend, named after the id
<DateInput id="passport-issued" legend="When was your passport issued?" />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">When was your passport issued?</legend><div class="fune-date-input" id="passport-issued"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-day">Day</label><input class="fune-input fune-input--width-2" id="passport-issued-day" type="text" inputMode="numeric" name="passport-issued-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-month">Month</label><input class="fune-input fune-input--width-2" id="passport-issued-month" type="text" inputMode="numeric" name="passport-issued-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-year">Year</label><input class="fune-input fune-input--width-4" id="passport-issued-year" type="text" inputMode="numeric" name="passport-issued-year"/></div></div></div></fieldset></div> -
a hint describes the group, GOV.UK's example
<DateInput id="passport-issued" legend="When was your passport issued?" hint="For example, 27 3 2007" isPageHeading />renders
<div class="fune-field"><fieldset class="fune-fieldset" aria-describedby="passport-issued-hint"><legend class="fune-legend fune-legend--heading"><h1 class="fune-legend-heading">When was your passport issued?</h1></legend><div class="fune-hint" id="passport-issued-hint">For example, 27 3 2007</div><div class="fune-date-input" id="passport-issued"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-day">Day</label><input class="fune-input fune-input--width-2" id="passport-issued-day" type="text" inputMode="numeric" name="passport-issued-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-month">Month</label><input class="fune-input fune-input--width-2" id="passport-issued-month" type="text" inputMode="numeric" name="passport-issued-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-year">Year</label><input class="fune-input fune-input--width-4" id="passport-issued-year" type="text" inputMode="numeric" name="passport-issued-year"/></div></div></div></fieldset></div> -
a date of birth autocompletes each box
<DateInput id="dob" legend="What is your date of birth?" birthday />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">What is your date of birth?</legend><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--width-2" id="dob-day" type="text" inputMode="numeric" autoComplete="bday-day" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" autoComplete="bday-month" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--width-4" id="dob-year" type="text" inputMode="numeric" autoComplete="bday-year" name="dob-year"/></div></div></div></fieldset></div> -
birthday false adds no autocomplete
<DateInput id="dob" legend="What is your date of birth?" birthday={false} />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">What is your date of birth?</legend><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--width-2" id="dob-day" type="text" inputMode="numeric" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--width-4" id="dob-year" type="text" inputMode="numeric" name="dob-year"/></div></div></div></fieldset></div> -
an error with no fields named marks the whole date
<DateInput id="dob" legend="Date of birth" error="Enter date of birth" />renders
<div class="fune-field fune-field--error"><fieldset class="fune-fieldset" aria-describedby="dob-error"><legend class="fune-legend">Date of birth</legend><p class="fune-error-message" id="dob-error"><span class="fune-visually-hidden">Error:</span> Enter date of birth</p><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--error fune-input--width-2" id="dob-day" type="text" inputMode="numeric" aria-invalid="true" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--error fune-input--width-2" id="dob-month" type="text" inputMode="numeric" aria-invalid="true" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--error fune-input--width-4" id="dob-year" type="text" inputMode="numeric" aria-invalid="true" name="dob-year"/></div></div></div></fieldset></div> -
an error with an empty list of fields marks the whole date too
<DateInput id="dob" legend="Date of birth" error="Enter date of birth" errorFields={[]} />renders
<div class="fune-field fune-field--error"><fieldset class="fune-fieldset" aria-describedby="dob-error"><legend class="fune-legend">Date of birth</legend><p class="fune-error-message" id="dob-error"><span class="fune-visually-hidden">Error:</span> Enter date of birth</p><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--error fune-input--width-2" id="dob-day" type="text" inputMode="numeric" aria-invalid="true" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--error fune-input--width-2" id="dob-month" type="text" inputMode="numeric" aria-invalid="true" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--error fune-input--width-4" id="dob-year" type="text" inputMode="numeric" aria-invalid="true" name="dob-year"/></div></div></div></fieldset></div> -
errorFields from validateDateParts marks only the year
<DateInput id="passport-issued" legend="When was your passport issued?" hint="For example, 27 3 2007" error="The date your passport was issued must include a year" errorFields={["year"]} value={{"day":"6","month":"3","year":""}} />renders
<div class="fune-field fune-field--error"><fieldset class="fune-fieldset" aria-describedby="passport-issued-hint passport-issued-error"><legend class="fune-legend">When was your passport issued?</legend><div class="fune-hint" id="passport-issued-hint">For example, 27 3 2007</div><p class="fune-error-message" id="passport-issued-error"><span class="fune-visually-hidden">Error:</span> The date your passport was issued must include a year</p><div class="fune-date-input" id="passport-issued"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-day">Day</label><input class="fune-input fune-input--width-2" id="passport-issued-day" type="text" inputMode="numeric" name="passport-issued-day" value="6"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-month">Month</label><input class="fune-input fune-input--width-2" id="passport-issued-month" type="text" inputMode="numeric" name="passport-issued-month" value="3"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="passport-issued-year">Year</label><input class="fune-input fune-input--error fune-input--width-4" id="passport-issued-year" type="text" inputMode="numeric" aria-invalid="true" name="passport-issued-year" value=""/></div></div></div></fieldset></div> -
two fields in error
<DateInput id="dob" legend="Date of birth" error="Date of birth must include a day and year" errorFields={["day","year"]} />renders
<div class="fune-field fune-field--error"><fieldset class="fune-fieldset" aria-describedby="dob-error"><legend class="fune-legend">Date of birth</legend><p class="fune-error-message" id="dob-error"><span class="fune-visually-hidden">Error:</span> Date of birth must include a day and year</p><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--error fune-input--width-2" id="dob-day" type="text" inputMode="numeric" aria-invalid="true" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--error fune-input--width-4" id="dob-year" type="text" inputMode="numeric" aria-invalid="true" name="dob-year"/></div></div></div></fieldset></div> -
errorFields without an error marks nothing
<DateInput id="dob" legend="Date of birth" errorFields={["day"]} />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">Date of birth</legend><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--width-2" id="dob-day" type="text" inputMode="numeric" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--width-4" id="dob-year" type="text" inputMode="numeric" name="dob-year"/></div></div></div></fieldset></div> -
a name of its own prefixes the boxes' names, ids stay
<DateInput id="f7" name="dob" legend="Date of birth" />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">Date of birth</legend><div class="fune-date-input" id="f7"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="f7-day">Day</label><input class="fune-input fune-input--width-2" id="f7-day" type="text" inputMode="numeric" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="f7-month">Month</label><input class="fune-input fune-input--width-2" id="f7-month" type="text" inputMode="numeric" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="f7-year">Year</label><input class="fune-input fune-input--width-4" id="f7-year" type="text" inputMode="numeric" name="dob-year"/></div></div></div></fieldset></div>
Show the other 4 tests
-
a default value fills the boxes
<DateInput id="dob" legend="Date of birth" defaultValue={{"day":"27","month":"jan","year":"2007"}} />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">Date of birth</legend><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--width-2" id="dob-day" type="text" inputMode="numeric" name="dob-day" value="27"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" name="dob-month" value="jan"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--width-4" id="dob-year" type="text" inputMode="numeric" name="dob-year" value="2007"/></div></div></div></fieldset></div> -
disabled, with a class name and autocomplete
<DateInput id="dob" legend="Date of birth" disabled birthday className="fune-field--compact" />renders
<div class="fune-field fune-field--compact"><fieldset class="fune-fieldset"><legend class="fune-legend">Date of birth</legend><div class="fune-date-input" id="dob"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-day">Day</label><input class="fune-input fune-input--width-2" id="dob-day" type="text" inputMode="numeric" autoComplete="bday-day" disabled="" name="dob-day"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-month">Month</label><input class="fune-input fune-input--width-2" id="dob-month" type="text" inputMode="numeric" autoComplete="bday-month" disabled="" name="dob-month"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="dob-year">Year</label><input class="fune-input fune-input--width-4" id="dob-year" type="text" inputMode="numeric" autoComplete="bday-year" disabled="" name="dob-year"/></div></div></div></fieldset></div> -
values are escaped
<DateInput id="d" legend="Start & end" value={{"day":"<1>","month":"\"2\"","year":"a&b"}} />renders
<div class="fune-field"><fieldset class="fune-fieldset"><legend class="fune-legend">Start & end</legend><div class="fune-date-input" id="d"><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="d-day">Day</label><input class="fune-input fune-input--width-2" id="d-day" type="text" inputMode="numeric" name="d-day" value="<1>"/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="d-month">Month</label><input class="fune-input fune-input--width-2" id="d-month" type="text" inputMode="numeric" name="d-month" value=""2""/></div></div><div class="fune-date-input-item"><div class="fune-field"><label class="fune-label" for="d-year">Year</label><input class="fune-input fune-input--width-4" id="d-year" type="text" inputMode="numeric" name="d-year" value="a&b"/></div></div></div></fieldset></div> -
an id with a space is refused
<DateInput id="date of birth" legend="Date of birth" />error: a field id cannot contain spaces
More from the author
The same `validateDateParts` runs in a Python or Rust API on the posted `dob-day`, `dob-month` and `dob-year`, with the same messages.
- **ids and names**: the `fune-date-input` wrapper is `id` (as GOV.UK), the hint and error `<id>-hint` and `<id>-error`, the boxes `<id>-day`, `<id>-month`, `<id>-year` (so an error summary can link to `#dob-day`); their names are `<name>-day` and so on, `name` defaulting to the id. - **Boxes in error**: with an `error`, the boxes listed in `errorFields` get `fune-input--error` and `aria-invalid="true"`; with none listed, all three do, which is GOV.UK's style for an error about the whole date. Without an `error`, nothing is marked. - **Keyboard**: `type="text"` with `inputMode="numeric"`, as GOV.UK does: `type="number"` rounds, scrolls and refuses "jan", which GOV.UK asks services to accept. - **birthday** adds `autoComplete` `bday-day`, `bday-month`, `bday-year` (WCAG 2.2, 1.3.5 Identify Input Purpose). - **value** with **onChange** is controlled; **defaultValue** alone is uncontrolled. Either way `onChange` receives all three boxes' text (`{ day, month, year }`), untrimmed, never the event. - **onBlur** is called, with nothing, whenever any of the three boxes loses focus, so a form can mark the date touched (`form.state`'s blur action). - It keeps the boxes' text in state for onChange, so it is a client component and its file starts with `"use client"`.
GOV.UK also puts `role="group"` on the date's `<fieldset>`; `react.form.fieldset` 1.0.0 does not take a role, so this does not either. A fieldset is already a group to assistive technology, so nothing is lost.
Classes: those of `react.form.fieldset` and `react.form.form-field` (each box sits in its own `fune-field` with a `fune-label`), plus `fune-date-input`, `fune-date-input-item`, `fune-input` (and `fune-input--error`), `fune-input--width-2` (day, month) and `fune-input--width-4` (year). Style a box inside a date with `.fune-date-input .fune-input`.
Sources: GOV.UK Design System, "Date input" https://design-system.service.gov.uk/components/date-input/ and the "Dates" pattern https://design-system.service.gov.uk/patterns/dates/; WCAG 2.2, 1.3.5 Identify Input Purpose https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.
Files
| Path | Bytes |
|---|---|
| README.md | 3,287 |
| impl/typescript.tsx | 3,269 |
| vectors.json | 16,783 |