react.form.text-field
A labelled single-line text input with hint, error message, autocomplete, prefix and suffix (GOV.UK text 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 single-line text input with its label, an optional hint and error message, and everything that makes it accessible wired for you: the label's `for`, the input's `aria-describedby` (hint, then error) and `aria-invalid` when there is an error. It is the GOV.UK Design System text input, as a React component.
"use client";
import { TextField } from "#fune/react.form.text-field@^1";
<TextField id="email" label="Email address" type="email" autoComplete="email" spellCheck={false}
hint="We'll only use this to send you a receipt" error={errors.email}
value={email} onChange={setEmail} />
For example
-
a plain text input: type text, submitted under its id
<TextField id="full-name" label="Full name" />renders
<div class="fune-field"><label class="fune-label" for="full-name">Full name</label><input class="fune-input" id="full-name" type="text" name="full-name"/></div> -
a name of its own
<TextField id="f1" name="fullName" label="Full name" />renders
<div class="fune-field"><label class="fune-label" for="f1">Full name</label><input class="fune-input" id="f1" type="text" name="fullName"/></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 Rust implementation, so it is shown in TypeScript. A Rust project cannot use it: fune build stops and names where it was required. Your choice of Rust is kept for every other page.
export function TextField(props: TextFieldProps): JSX.Element
Its props, TextFieldProps. A ? marks one the caller may leave out.
| label | ReactNode | the question, or the field's name |
| id? | string | the input's id; React's useId() when left out |
| name? | string | what the value is submitted as; the id when left out |
| type? | TextFieldType | text when left out; email, tel and url bring the right phone keyboard |
| hint? | ReactNode | help under the label |
| error? | ReactNode | what is wrong; marks the input invalid and describes it |
| value? | string | the value, for a controlled input (with onChange) |
| defaultValue? | string | the starting value, for an uncontrolled one |
| onChange? | (value: string) => void | called with the new value as the person types |
| onBlur? | () => void | called when the input loses focus, to mark the field touched |
| autoComplete? | string | the autocomplete token, such as email, tel, name or postal-code (WCAG 1.3.5) |
| inputMode? | string | the keyboard to show, such as numeric for a code that is all digits |
| spellCheck? | boolean | false for names, emails and codes, which a spell checker would mark wrong |
| prefix? | ReactNode | shown before the input, such as £; hidden from screen readers, so the label says it too |
| suffix? | ReactNode | shown after the input, such as "per item"; hidden from screen readers too |
| required? | boolean | the browser refuses an empty value |
| disabled? | boolean | shown but not editable, and not submitted |
| className? | string | added to the wrapper's classes |
| renders | JSX.Element |
The types it declares, generated into your project
export type TextFieldType = "text" | "email" | "tel" | "url" | "search";
/** One question answered in a line of text. */
export interface TextFieldProps {
/** the question, or the field's name */
readonly label: ReactNode;
/** the input's id; React's useId() when left out */
readonly id?: string;
/** what the value is submitted as; the id when left out */
readonly name?: string;
/** text when left out; email, tel and url bring the right phone keyboard */
readonly type?: TextFieldType;
/** help under the label */
readonly hint?: ReactNode;
/** what is wrong; marks the input invalid and describes it */
readonly error?: ReactNode;
/** the value, for a controlled input (with onChange) */
readonly value?: string;
/** the starting value, for an uncontrolled one */
readonly defaultValue?: string;
/** called with the new value as the person types */
readonly onChange?: (value: string) => void;
/** called when the input loses focus, to mark the field touched */
readonly onBlur?: () => void;
/** the autocomplete token, such as email, tel, name or postal-code (WCAG 1.3.5) */
readonly autoComplete?: string;
/** the keyboard to show, such as numeric for a code that is all digits */
readonly inputMode?: string;
/** false for names, emails and codes, which a spell checker would mark wrong */
readonly spellCheck?: boolean;
/** shown before the input, such as £; hidden from screen readers, so the label says it too */
readonly prefix?: ReactNode;
/** shown after the input, such as "per item"; hidden from screen readers too */
readonly suffix?: ReactNode;
/** the browser refuses an empty value */
readonly required?: 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 { TextField } from "#fune/react.form.text-field@^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, type JSX, type ReactNode } from "react";
import { FormField, fieldIds } from "./react_form_form_field.ts"; ← from react.form.form-field ^1.0.0 · built alongside by fune
import type { TextFieldProps } from "./react_form_text_field_types.ts";
function present(node: ReactNode): boolean {
return node !== null && node !== undefined && node !== false && node !== "";
}
/**
* The GOV.UK text input: a label, an optional hint and error message, and the
* input, described by both. A prefix or suffix sits in a wrapper beside the
* input and is aria-hidden, as GOV.UK does, so the label has to say it too
* ("Cost, in pounds").
*/
export function TextField(props: TextFieldProps): JSX.Element {
const auto = useId();
const { label, hint, error, value, defaultValue, onChange, onBlur, autoComplete, inputMode, spellCheck, prefix, suffix, required, disabled, className } = props;
const id = props.id ?? auto;
const ids = fieldIds(id, present(hint), present(error));
const input = (
<input
className={ids.error ? "fune-input fune-input--error" : "fune-input"}
id={ids.control}
name={props.name ?? id}
type={props.type ?? "text"}
value={value}
defaultValue={defaultValue}
autoComplete={autoComplete}
inputMode={inputMode as JSX.IntrinsicElements["input"]["inputMode"]}
spellCheck={spellCheck}
required={required}
disabled={disabled}
aria-describedby={ids.describedBy ?? undefined}
aria-invalid={ids.error ? true : undefined}
onChange={(event) => onChange?.(event.target.value)}
onBlur={onBlur ? () => onBlur() : undefined}
/>
);
return (
<FormField id={id} label={label} hint={hint} error={error} className={className}>
{present(prefix) || present(suffix) ? (
<div className="fune-input-wrapper">
{present(prefix) ? (
<div className="fune-input-prefix" aria-hidden="true">
{prefix}
</div>
) : null}
{input}
{present(suffix) ? (
<div className="fune-input-suffix" aria-hidden="true">
{suffix}
</div>
) : null}
</div>
) : (
input
)}
</FormField>
);
}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 1 dependency, 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.text-field
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.text-field-1.0.1-typescript.fune, or fetch it from a terminal with fune pull react.form.text-field@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.text-field-1.0.1.fune, 18,043 bytes, sha256 8fadff2c32a9f4cadc2147e799a5b885037c65616cbd70d4228dfb8eddd34f54.
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.text-field
after — your function gets the result and the arguments, and returns the final result.
// fune: after react.form.text-field
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 react.form.form-field in react.form.text-field
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.text-field --steps.
// fune: step react.form.text-field 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.
-
a plain text input: type text, submitted under its id
<TextField id="full-name" label="Full name" />renders
<div class="fune-field"><label class="fune-label" for="full-name">Full name</label><input class="fune-input" id="full-name" type="text" name="full-name"/></div> -
a name of its own
<TextField id="f1" name="fullName" label="Full name" />renders
<div class="fune-field"><label class="fune-label" for="f1">Full name</label><input class="fune-input" id="f1" type="text" name="fullName"/></div> -
an email address: type, autocomplete and no spell check
<TextField id="email" label="Email address" type="email" autoComplete="email" spellCheck={false} />renders
<div class="fune-field"><label class="fune-label" for="email">Email address</label><input class="fune-input" id="email" type="email" autoComplete="email" spellCheck="false" name="email"/></div> -
a hint describes the input
<TextField id="phone" label="UK telephone number" type="tel" hint="For international numbers include the country code" autoComplete="tel" />renders
<div class="fune-field"><label class="fune-label" for="phone">UK telephone number</label><div class="fune-hint" id="phone-hint">For international numbers include the country code</div><input class="fune-input" id="phone" type="tel" autoComplete="tel" aria-describedby="phone-hint" name="phone"/></div> -
an error marks the input invalid and describes it
<TextField id="email" label="Email address" type="email" error="Enter an email address in the correct format, like name@example.com" />renders
<div class="fune-field fune-field--error"><label class="fune-label" for="email">Email address</label><p class="fune-error-message" id="email-error"><span class="fune-visually-hidden">Error:</span> Enter an email address in the correct format, like name@example.com</p><input class="fune-input fune-input--error" id="email" type="email" aria-describedby="email-error" aria-invalid="true" name="email"/></div> -
hint and error: described by both, hint first
<TextField id="url" label="Website" type="url" hint="Starting with https://" error="Enter a website address, like https://example.com" value="example" />renders
<div class="fune-field fune-field--error"><label class="fune-label" for="url">Website</label><div class="fune-hint" id="url-hint">Starting with https://</div><p class="fune-error-message" id="url-error"><span class="fune-visually-hidden">Error:</span> Enter a website address, like https://example.com</p><input class="fune-input fune-input--error" id="url" type="url" aria-describedby="url-hint url-error" aria-invalid="true" name="url" value="example"/></div> -
a controlled value
<TextField id="town" label="Town or city" value="Leeds" autoComplete="address-level2" />renders
<div class="fune-field"><label class="fune-label" for="town">Town or city</label><input class="fune-input" id="town" type="text" autoComplete="address-level2" name="town" value="Leeds"/></div> -
a default value renders as the value attribute
<TextField id="q" label="Search" type="search" defaultValue="vat" />renders
<div class="fune-field"><label class="fune-label" for="q">Search</label><input class="fune-input" id="q" type="search" name="q" value="vat"/></div> -
a prefix and a suffix wrap the input and are hidden from screen readers
<TextField id="cost" label="Cost, in pounds, per item" prefix="£" suffix="per item" inputMode="decimal" spellCheck={false} />renders
<div class="fune-field"><label class="fune-label" for="cost">Cost, in pounds, per item</label><div class="fune-input-wrapper"><div class="fune-input-prefix" aria-hidden="true">£</div><input class="fune-input" id="cost" type="text" inputMode="decimal" spellCheck="false" name="cost"/><div class="fune-input-suffix" aria-hidden="true">per item</div></div></div> -
a prefix alone
<TextField id="amount" label="Amount, in pounds" prefix="£" />renders
<div class="fune-field"><label class="fune-label" for="amount">Amount, in pounds</label><div class="fune-input-wrapper"><div class="fune-input-prefix" aria-hidden="true">£</div><input class="fune-input" id="amount" type="text" name="amount"/></div></div>
Show the other 4 tests
-
required and disabled are boolean attributes
<TextField id="ref" label="Reference" required disabled />renders
<div class="fune-field"><label class="fune-label" for="ref">Reference</label><input class="fune-input" id="ref" type="text" required="" disabled="" name="ref"/></div> -
false booleans and an empty suffix render nothing
<TextField id="ref" label="Reference" required={false} disabled={false} suffix="" className="fune-field--narrow" />renders
<div class="fune-field fune-field--narrow"><label class="fune-label" for="ref">Reference</label><input class="fune-input" id="ref" type="text" name="ref"/></div> -
values and labels are escaped
<TextField id="c" label="Company \"name\" & <number>" value="A&B \"Ltd\"" />renders
<div class="fune-field"><label class="fune-label" for="c">Company "name" & <number></label><input class="fune-input" id="c" type="text" name="c" value="A&B "Ltd""/></div> -
an id with a space is refused
<TextField id="full name" label="Full name" />error: a field id cannot contain spaces
More from the author
- **type** is `text`, `email`, `tel`, `url` or `search`. The last four bring the matching keyboard on a phone. There is no `number`: GOV.UK advises against it (it rounds, and scrolls away a value); use `inputMode="numeric"` with a text input for a number that is really a code. - **autoComplete** takes the HTML token (`email`, `tel`, `name`, `postal-code`, `one-time-code`...), which lets browsers fill the field and meets WCAG 2.2 success criterion 1.3.5, Identify Input Purpose. - **prefix** and **suffix** (`£`, `per item`) sit beside the input in a wrapper and are `aria-hidden`, exactly as GOV.UK does, because a screen reader would read them out of context. Say the unit in the label as well. - **value** with **onChange** is a controlled input; **defaultValue** alone is an uncontrolled one, fine for a plain `<form>` post. `onChange` is called with the new value, not the event. Do not pass both value and defaultValue. - **id** is the input's id and, unless **name** says otherwise, the name it is submitted under. Left out, it is React's `useId()`; pass one whenever an error summary has to link to the field.
It is a client component (it listens for input), so the built file starts with `"use client"`: import it from a server page as it is, and pass it no functions there.
Classes: those of `react.form.form-field`, plus `fune-input` (and `fune-input--error`), `fune-input-wrapper`, `fune-input-prefix`, `fune-input-suffix`.
Sources: GOV.UK Design System, "Text input" https://design-system.service.gov.uk/components/text-input/; WCAG 2.2, 1.3.5 Identify Input Purpose https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose; HTML, autofill tokens https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill.
## 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
| Path | Bytes |
|---|---|
| NOTICE | 1,219 |
| README.md | 2,712 |
| impl/typescript.tsx | 2,206 |
| vectors.json | 5,893 |