Functional Weave
Code in Rust

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.0 (not the latest) · 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

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

labelReactNodethe question, or the field's name
id?stringthe input's id; React's useId() when left out
name?stringwhat the value is submitted as; the id when left out
type?TextFieldTypetext when left out; email, tel and url bring the right phone keyboard
hint?ReactNodehelp under the label
error?ReactNodewhat is wrong; marks the input invalid and describes it
value?stringthe value, for a controlled input (with onChange)
defaultValue?stringthe starting value, for an uncontrolled one
onChange?(value: string) => voidcalled with the new value as the person types
onBlur?() => voidcalled when the input loses focus, to mark the field touched
autoComplete?stringthe autocomplete token, such as email, tel, name or postal-code (WCAG 1.3.5)
inputMode?stringthe keyboard to show, such as numeric for a code that is all digits
spellCheck?booleanfalse for names, emails and codes, which a spell checker would mark wrong
prefix?ReactNodeshown before the input, such as £; hidden from screen readers, so the label says it too
suffix?ReactNodeshown after the input, such as "per item"; hidden from screen readers too
required?booleanthe browser refuses an empty value
disabled?booleanshown but not editable, and not submitted
className?stringadded to the wrapper's classes
rendersJSX.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";
impl/typescript.tsx · 62 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, 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
Download for TypeScript react.form.text-field-1.0.0-typescript.fune · 16,501 bytes sha256 245ab41773b6dfdc6653cfbefa4eef91eceececa81069f358041cb9479550094

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./react.form.text-field-1.0.0-typescript.fune, or fetch it from a terminal with fune pull react.form.text-field@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.text-field-1.0.0.fune, 16,473 bytes, sha256 73865468de96ff4fdf0fc522dcc9449765bcc074ca4037609b6f9cacc3a57d1f.

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.

  1. 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>
  2. 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>
  3. 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>
  4. 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>
  5. 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>
  6. 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>
  7. 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>
  8. 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>
  9. 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>
  10. 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
  1. 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>
  2. 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>
  3. 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 &quot;name&quot; &amp; &lt;number&gt;</label><input class="fune-input" id="c" type="text" name="c" value="A&amp;B &quot;Ltd&quot;"/></div>
  4. 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.

Files

PathBytes
README.md2,413
impl/typescript.tsx2,206
vectors.json5,893