Skip to main content

Design system components

TextInput

Labeled text input with validation, helper text, and size tokens.

Status: stable. Group: form. Markdown version: /design-system/components/text-input.md

When to use

TextInput is the standard single-line field: label, control, helper and error text in one component. The email and tel types add built-in validation (typo suggestions for common email domains, libphonenumber formatting for phone numbers); error renders above the helper line and wires aria-invalid.

When not to use

  • Do not ship a form that relies on the email validator as the only verification step. The validator is for UX; the server must verify too.
  • Do not pass a value plus a defaultValue. The component treats value as controlled; mixing them produces drift.
  • Do not use TextInput for multi-line text. Use TextArea, which shares the label + helper contract but carries multi-line UX.

Look-alikes

  • Combobox: Do not use Combobox for free text with suggestions; it only emits option values (no free-solo mode). Use TextInput plus your own suggestion list.
  • PhoneInput: Do not use TextInput type=tel for international audiences; you lose country selection and normalization.
  • Select: Do not use Select for a freeform value with suggestions; use TextInput.
  • TextArea: Do not build multi-line entry with TextInput; use TextArea.

Props

TextInput props
PropTypeDefaultDescriptionRequired
labelstringnoneVisible field label; also feeds the input name (lowercased, spaces to hyphens) unless a name prop is passed.Yes
type"number" | "search" | "text" | "tel" | "email" | "date" | "time" | "password" | "datetime-local" | "month" | "week"noneNative HTML input type. Telephone values format as the user types; telephone and email values validate when the field first loses focus and then live as the value changes. Date/time variants retain the browser's localized picker and ISO-compatible submitted value.Yes
size"sm" | "md" | "lg" | "S" | "M" | "L"mdSize variant for input. Defaults to md.No
onValueChange(value: string | number) => voidnoneValue change handler (recommended)No
defaultValuestring | numbernoneInitial value for uncontrolled component.No
valuestring | numbernoneControlled value; an initial "" is treated as absent (uncontrolled), later changes always sync in.No
errorstringnoneValidation error message; renders above the helper line and sets aria-invalid.No
statusFieldStatusnoneSemantic state: colours the border and styles statusMessage. error overrides it.No
statusMessagestringnoneMessage under the field for status, above the helper line.No
actionsFieldAction[]noneIcon buttons at the end of the label row: each runs an operation or opens a menu.No
helperTextstringnoneHelper copy below the field.No
disabledbooleanfalseDisables the input. Declared explicitly (not just via the native attribute extension) so the agent-blocks prop extraction sees it.No
clearablebooleanfalseShows a labelled ×-button inside the field chrome while the field has a value; clearing returns focus to the input. Defaults to false.No
onClear() => voidnoneCalled after the clear button empties the field (onValueChange also fires with "")No
hideLabelbooleanfalseVisually hides the label (kept in the accessibility tree); use where surrounding design carries the affordance. Defaults to false.No
width"full" | "2ch" | "3ch" | "4ch" | "5ch" | "10ch" | "20ch" | "30ch"fullWidest the field grows, in characters of the expected answer (after the GOV.UK input widths): "10ch" for a code, "30ch" for an email address. The field still shrinks with its container. Defaults to full.No
onChange(value: string | number) => voidnoneNo description in the contract. Deprecated: Deprecated.No

Variants

  • Available sizes: sm, md, lg (default md). Forwarded to CSS module size classes.
  • Available types: number, search, text, tel, email, date, time, password, datetime-local, month, week.
  • Allowed width values: full, 2ch, 3ch, 4ch, 5ch, 10ch, 20ch, 30ch (default full).

Invalid combinations

  • MUST NOT: value and defaultValue set together. value makes the component controlled and defaultValue uncontrolled; pass one, not both.

Keyboard

TextInput keyboard behaviour
KeyBehaviour
TabMoves focus to the control and on to the next focusable element. (standard)

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

TextInput rules
LevelRuleHow it is checked
MUST NOTvalue and defaultValue set together. value makes the component controlled and defaultValue uncontrolled; pass one, not both.Checked by the MCP tool validate_component_usage.
MUSTARIA: Label association.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Aria-describedby for error/helper; a consumer aria-describedby is merged after the status and helper ids, while a consumer aria-invalid replaces the computed one.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Built-in email and tel validation waits for the first blur, then re-runs on every change; the error renders through HelperText with role=alert, so a half-typed value is not announced and the error clears as soon as the value is valid.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Clear button carries a per-field accessible name and returns focus to the input.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDAlways pass a real label; placeholders vanish on focus and are not accessible names.Not stated in the contract.
SHOULDRead values through onValueChange; it hands you the typed value directly (onChange is deprecated).Not stated in the contract.
SHOULDPick the semantic type; email and tel get validation and the right mobile keyboard for free.Not stated in the contract.
SHOULDUse helperText for format hints before errors happen (We never share this address).Not stated in the contract.
SHOULDUse clearable for fields users iterate on (search boxes, filters, generators); the ×-button appears only while there is a value and hands focus back to the input.Not stated in the contract.
SHOULDReserve hideLabel for dense compositions where surrounding design carries the affordance; the label stays in the accessibility tree, so still write a real one.Not stated in the contract.
SHOULD NOTDo not build multi-line entry with TextInput; use TextArea.Not stated in the contract.
SHOULD NOTDo not layer your own live email/tel validation on top of the built-in one; two messages can disagree. Server-side truth belongs in error after submit.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { TextInput } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/forms exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-text-input>; see the custom elements manifest.