Functional Weave
Code in Python

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.1 · 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

  1. 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>
  2. 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.

A React capability: React ^19 · TypeScript only. This capability has no Python implementation, so it is shown in TypeScript. A Python project cannot use it: fune build stops and names where it was required. Your choice of Python is kept for every other page.

export function DateInput(props: DateInputProps): JSX.Element

Its props, DateInputProps. A ? marks one the caller may leave out.

legendReactNodethe question, such as "What is your date of birth?"
id?stringthe 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?stringthe start of the boxes' names, <name>-day and so on; the id when left out
hint?ReactNodehelp under the legend, such as "For example, 27 3 2007"
error?ReactNodewhat 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?DatePartsthe three boxes' text, for a controlled input (with onChange)
defaultValue?DatePartsthe starting text, for an uncontrolled one
onChange?(value: DateParts) => voidcalled with all three boxes' text whenever one changes
onBlur?() => voidcalled when any of the boxes loses focus, to mark the field touched
birthday?booleana date of birth: the boxes autocomplete as bday-day, bday-month and bday-year
isPageHeading?booleanthe legend is the page's heading
disabled?booleanshown but not editable, and not submitted
className?stringadded to the wrapper's classes
rendersJSX.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";
impl/typescript.tsx · 72 lines · open · raw

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
Download for TypeScript react.form.date-input-1.0.1-typescript.fune · 31,665 bytes sha256 25d274947e525e46adf5bb33b7c7e67f599c17f76ffd9d3fe115bd6afbddea4b

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.date-input-1.0.1-typescript.fune, or fetch it from a terminal with fune pull react.form.date-input@1.0.1: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.1.fune, 31,637 bytes, sha256 a734e5f2e2bc4c8a5d4cc58279b1e22528b3259540e552286afc44c5677074a6.

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.

  1. 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>
  2. 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>
  3. 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>
  4. 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>
  5. 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>
  6. 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>
  7. 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>
  8. 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>
  9. 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>
  10. 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
  1. 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>
  2. 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>
  3. 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 &amp; 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="&lt;1&gt;"/></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="&quot;2&quot;"/></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&amp;b"/></div></div></div></fieldset></div>
  4. 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.

## Notices

Portions derived from GOV.UK Frontend (https://github.com/alphagov/govuk-frontend), Copyright (c) 2017 Crown Copyright (Government Digital Service), under the MIT License; the full notice is in NOTICE.

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE1,219
README.md3,586
impl/typescript.tsx3,269
vectors.json16,783