{"slug":"text-field","name":"TextField","packageName":"@liquidify/react","category":"controls","maturity":"experimental","summary":"A user-editable control for entering freeform text — the canonical glass input field.","importPath":"@liquidify/react/text-field","sourcePath":"packages/@liquidify/react/src/components/text-field/text-field.tsx","specPath":"packages/@liquidify/react/src/components/text-field/spec.md","docsPath":"../liquidify-page/content/component-source/text-field.md","documentationStatus":"draft","storyIds":["controls-text-field--default"],"additionalExports":["LineLimit","LiquidColor","LiquidTintValue","TextFieldMultilineProps","TextFieldSingleLineProps","TextFieldSizing","TextFieldType","TextFieldVariant","TintableControlProps"],"anatomy":[],"props":[{"name":"autoComplete","type":"string | undefined","required":false,"description":"Native `autocomplete` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"className","type":"string | undefined","required":false,"description":"Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"clearable","type":"boolean | undefined","required":false,"description":"When `true`, a trailing clear button renders whenever the resolved value is non-empty and the field is neither `disabled` nor `readOnly`. Pressing it commits `\"\"` through the controlled triad, fires {@link TextFieldChromeProps.onClear}, and returns focus to the field. `Escape` takes the same path.","defaultValue":"`false`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"clearLabel","type":"string | undefined","required":false,"description":"Accessible name of the clear button.","defaultValue":"`\"Clear text\"`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"defaultValue","type":"string | undefined","required":false,"description":"Uncontrolled seed for the internal value (ignored once {@link TextSurfaceProps.value} is supplied). Read once, on mount.","defaultValue":"`\"\"`","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"disabled","type":"boolean | undefined","required":false,"description":"When `true`, the native `disabled` attribute is set on the inner element: the surface is removed from the tab order and all input is suppressed. `aria-disabled` is NOT set — the native attribute already provides full-suppression semantics.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"distribution","type":"\"fill\" | \"fit\" | undefined","required":false,"description":"Layout of the field's DOM root — the TextField sibling of SegmentedControl's `distribution` axis (`NSSegmentedControl.Distribution` pedigree, charter §5). `\"fill\"` fills the container width; `\"fit\"` hugs its content. Resolved via the `data-distribution` attribute selector in `styles.css`.","defaultValue":"`\"fill\"`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"icon","type":"ReactNode","required":false,"description":"Optional decorative node rendered before the text — the in-field leading glyph (a magnifier for a search field), following the `Label` / `EmptyState` icon convention. Rendered `aria-hidden`, so it is never the accessible name: still supply `aria-label` / `aria-labelledby`. On the multiline branch it aligns to the first text row rather than the box's vertical centre.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"id","type":"string | undefined","required":false,"description":"Explicit id for the native element. Falls back to a generated stable id so an external `<label htmlFor>` or error message can target it (A16).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"inputMode","type":"TextInputMode | undefined","required":false,"description":"Native `inputmode` passthrough — hints the on-screen keyboard variant.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"invalid","type":"boolean | undefined","required":false,"description":"Marks the surface invalid — sets `aria-invalid=\"true\"` for AT / validation styling. An explicit `aria-invalid` on the same element always wins (including `false` / `\"grammar\"` / `\"spelling\"`); `invalid` is only the shorthand fallback.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"lineLimit","type":"LineLimit | undefined","required":false,"description":"Not on the single-line branch — `lineLimit` is multiline-only. See {@link TextFieldSingleLineProps.sizing}. Row-range bound — maps to SwiftUI's `.lineLimit(min...max)`. See {@link LineLimit} for the sanitisation policy applied at render time.","defaultValue":"`{ min: 2, max: 4 }`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"maxLength","type":"number | undefined","required":false,"description":"Native `maxlength` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"minLength","type":"number | undefined","required":false,"description":"Native `minlength` passthrough.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"multiline","type":"boolean | undefined","required":false,"description":"Discriminant: absent/`false` selects the single-line (`<input>`) branch. Discriminant: `true` selects the multiline (`<textarea>`) branch.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"name","type":"string | undefined","required":false,"description":"Native form field name. When set, the element participates in form submission and React Hook Form can register it (A16).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onBlur","type":"((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | ((event: FocusEvent<HTMLInputElement, Element>) => void) | undefined","required":false,"description":"Low-level native passthrough; fires on native blur of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onChange","type":"((next: string) => void) | undefined","required":false,"description":"Fires with the next string whenever the content changes — the SwiftUI `Binding<String>` write-back analogue. Called in both controlled and uncontrolled modes; never called while {@link TextSurfaceProps.disabled} or {@link TextSurfaceProps.readOnly}.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onClear","type":"(() => void) | undefined","required":false,"description":"Fires after the value is cleared via the clear button or `Escape`.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"onFocus","type":"((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | ((event: FocusEvent<HTMLInputElement, Element>) => void) | undefined","required":false,"description":"Low-level native passthrough; fires on native focus of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"onKeyDown","type":"((event: KeyboardEvent<HTMLInputElement>) => void) | ((event: KeyboardEvent<HTMLTextAreaElement>) => void) | undefined","required":false,"description":"Low-level native passthrough; fires on `keydown` of the `<input>`, **before** the component's own `Enter` / `Escape` handling — calling `event.preventDefault()` here vetoes it. Never fires while {@link TextSurfaceProps.disabled} is `true`. Low-level native passthrough; fires on `keydown` of the `<textarea>`, **before** the component's own `Escape` handling. See {@link TextFieldSingleLineProps.onKeyDown}.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"onSubmit","type":"((value: string) => void) | undefined","required":false,"description":"Fires with the current value when `Enter` is pressed — the SwiftUI `.onSubmit { … }` analogue and the search field's submit affordance. Native form submission is untouched (the component never calls `preventDefault`). Never fires while `disabled` or `readOnly`, nor when a caller's {@link TextFieldSingleLineProps.onKeyDown} prevented the event. Single-line only — multiline `Enter` inserts a newline. Not on the multiline branch — `Enter` inserts a newline in a `<textarea>`, so there is no submit key to hang `onSubmit` on. See {@link TextFieldMultilineProps.secure}.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"pattern","type":"string | undefined","required":false,"description":"Native validation pattern (a `RegExp` source string) — single-line only, the SwiftUI surface has no textarea pattern analogue. Not on the multiline branch — `pattern` is single-line-only. See {@link TextFieldMultilineProps.secure}.","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"placeholder","type":"string | undefined","required":false,"description":"Placeholder text shown when the surface is empty — the SwiftUI title-arg analogue (the `prompt` parameter).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"readOnly","type":"boolean | undefined","required":false,"description":"When `true`, the surface stays focusable and its value is submitted, but no edit is possible: the native `readOnly` attribute is set and `onChange` is hard-suppressed (never a silent CSS-only block).","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"ref","type":"((instance: HTMLTextAreaElement | null) => void | (() => VoidOrUndefinedOnly) | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | RefObject<HTMLTextAreaElement | null> | ((instance: HTMLInputElement | null) => void | (() => VoidOrUndefinedOnly) | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | RefObject<HTMLInputElement | null> | null | undefined","required":false,"description":"Ref forwarded to the native element — the surface's primary interactive element.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"required","type":"boolean | undefined","required":false,"description":"Marks the surface required — sets the native `required` + `aria-required`.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"secure","type":"boolean | undefined","required":false,"description":"When `true`, the element is `<input type=\"password\">` and entered text renders as platform masked dots. Single-line only — combining with `multiline` is a type error (was silently ignored pre-Phase-3). Not on the multiline branch — `secure` is single-line-only (SwiftUI has no `SecureField(axis: .vertical)`). Typed `never` for the same real- type-error reason as {@link TextFieldSingleLineProps.sizing}.","defaultValue":"`false`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"sizing","type":"TextFieldSizing | undefined","required":false,"description":"Not on the single-line branch — `sizing` is multiline-only. Typed `never` (rather than simply absent) so a caller combining it with a single-line field is a real type error, not a leniency gap in TypeScript's excess- property checking against union types. Multiline height behaviour — `\"fixed\"` locks to `lineLimit.min` rows, `\"relative\"` grows `min`→`max` rows then scrolls, `\"content\"` grows to fit all content. Resolved via `data-sizing` in CSS (multiline-only attribute).","defaultValue":"`\"relative\"`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"tint","type":"LiquidTintValue | undefined","required":false,"description":"System-accent of the control — drawn from the RESTRICTED {@link LIQUID_COLORS} palette (the thirteen Apple system colours). **Orthogonal to a component's `variant`**: it recolours the accent the surface draws from (focus ring, tinted fill, selection) without changing its variant. Resolved via `data-tint` attribute selectors in CSS (tokens-only, ADR-0003).","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"type","type":"TextFieldType | undefined","required":false,"description":"Native `type` of the `<input>` — single-line only. `\"search\"` is the search-field setting (the browser's own cancel button is suppressed in CSS so {@link TextFieldChromeProps.clearable} stays the single clear affordance). {@link TextFieldSingleLineProps.secure} wins when both are supplied. Not on the multiline branch — a `<textarea>` has no `type` attribute. Typed `never` for the same real-type-error reason as {@link TextFieldMultilineProps.secure}.","defaultValue":"`\"text\"`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"},{"name":"value","type":"string | undefined","required":false,"description":"Controlled current text value. Passing this switches the surface to controlled mode: the rendered value always reflects this prop and the consumer owns it via {@link TextSurfaceProps.onChange}.","declaredIn":"packages/@liquidify/react/src/shared/text-surface.ts"},{"name":"variant","type":"TextFieldVariant | undefined","required":false,"description":"Visual style of the field (the SwiftUI `.textFieldStyle(…)` family). `\"automatic\"`/`\"plain\"` are bare text containers (no glass); `\"rounded\"` draws the translucent-glass rounded-rect shell; `\"prominent\"` is a full-pill glassy \"main field\" for the one or two hero fields per screen.","defaultValue":"`\"rounded\"`","declaredIn":"packages/@liquidify/react/src/components/text-field/text-field.tsx"}],"swiftUIMap":[{"swiftUI":"`TextField(_ title:text:)` title arg","react":"`placeholder`","notes":"The title string is the field's placeholder/prompt. Demo values: `\"Placeholder\"`, `\"Editable\"`, `\"Multiline\"`.","relationship":"direct"},{"swiftUI":"`text: Binding<String>`","react":"`value` (controlled) / `defaultValue` (uncontrolled seed) + `onChange(next: string)`","notes":"String-bearing controlled triad (ADR-0015), wired through the shared `useControllableState` machine. A `Binding<String>` splits into controlled `value`+`onChange`, or uncontrolled `defaultValue` (read once, on mount). `onChange` fires the next string on every edit, in both modes, never while `disabled` or `readOnly`. Default `defaultValue=\"\"`.","relationship":"direct"},{"swiftUI":"`.textFieldStyle(.automatic | .plain | .roundedBorder)` + `prominent` (extension)","react":"`variant?: \"automatic\" | \"plain\" | \"rounded\" | \"prominent\"`","notes":"The SwiftUI style family → one `variant` prop, resolved via `data-variant` in CSS. Default `\"rounded\"`. `prominent` is a library extension (full-pill hero field) with no exact `.textFieldStyle` source (charter §6 rule 3).","relationship":"extension"},{"swiftUI":"`SecureField(_:text:)`","react":"`secure?: boolean` (single-line branch only)","notes":"Masked-dot rendering; renders as `<input type=\"password\">`. Same rounded shell as the default variant. Combining with `multiline` is now a type error. Wins over `type` when both are supplied (`secure` is the SwiftUI-sourced prop; `type` is the native escape hatch).","relationship":"direct"},{"swiftUI":"`.keyboardType` / `UISearchTextField` / native `<input type>`","react":"`type?: \"text\" | \"search\" | \"email\" | \"tel\" | \"url\" | \"password\"` (single-line branch only)","notes":"The native `type` attribute, default `\"text\"`. `\"search\"` is what makes a TextField a search field for AT and for platform affordances (the browser's own cancel button is suppressed in CSS so `clearable` is the single clear affordance). Independent of `inputMode`, which only hints the on-screen keyboard. `secure` takes precedence (`secure` → `type=\"password\"` regardless). Not on the multiline branch — a `<textarea>` has no `type`.","relationship":"direct"},{"swiftUI":"`Label(_:systemImage:)` inside the field / `.searchable` magnifier","react":"`icon?: ReactNode`","notes":"Optional decorative leading node, rendered `aria-hidden` in `.lq-text-field-icon` (the `Label` / `EmptyState` icon convention). Never the accessible name — pass `aria-label` separately. Available on both branches.","relationship":"direct"},{"swiftUI":"`.searchable` in-field clear / `UISearchTextField.clearButtonMode`","react":"`clearable?: boolean` + `onClear?()` + `clearLabel?: string`","notes":"`clearable` (default `false`) enables the trailing clear button; it renders only while the value is non-empty and the field is neither `disabled` nor `readOnly`. Pressing it commits `\"\"` through the controlled triad (`onChange(\"\")`), then fires `onClear()`, then refocuses the field. `clearLabel` is its accessible name, default `\"Clear text\"`. Available on both branches.","relationship":"direct"},{"swiftUI":"`.onSubmit { … }`","react":"`onSubmit?(value: string)` (single-line branch only)","notes":"Fires with the current value when `Enter` is pressed on a single-line field — the search-submit affordance. Native form submission is unaffected (the handler does not `preventDefault`). Never fires while `disabled`; never on the multiline branch, where `Enter` inserts a newline.","relationship":"direct"},{"swiftUI":"— (low-level native passthrough)","react":"`onKeyDown?(event)`","notes":"Raw `KeyboardEvent` passthrough on the branch's concrete element, called **before** the component's own `Enter` / `Escape` handling; calling `event.preventDefault()` in it suppresses that handling. Never fires while `disabled`.","relationship":"direct"},{"swiftUI":"— (no SwiftUI textarea pattern)","react":"`pattern?: string` (single-line branch only)","notes":"Native validation `RegExp` source passthrough.","relationship":"direct"},{"swiftUI":"`TextField(…, axis: .vertical)`","react":"`multiline?: false` (single-line) | `multiline: true` (multiline)","notes":"The union discriminant. Vertical-growing field; `multiline: true` renders as `<textarea>` and narrows `ref` to `Ref<HTMLTextAreaElement>`.","relationship":"direct"},{"swiftUI":"`.lineLimit(2...4)`","react":"`lineLimit?: { min: number; max: number }` (multiline branch only)","notes":"Row-range bound; demo uses `min: 2, max: 4`. **Sanitised at render**: a non-finite bound falls back to the `{2,4}` default, both bounds floor to an integer and clamp to a minimum of 1, and an inverted range (`min > max`) dev-warns once and clamps `max` up to `min` (audit §6.6 P2 — previously passed straight to `rows` + CSS vars unsanitised).","relationship":"direct"},{"swiftUI":"`.lineLimit(_:reservesSpace:)` / CSS `field-sizing`","react":"`sizing?: \"fixed\" | \"relative\" | \"content\"` (multiline branch only)","notes":"Multiline height behaviour: `fixed` locks to `lineLimit.min` rows; `relative` grows `min`→`max` then scrolls (default); `content` grows to fit all content. Resolved via `data-sizing` in CSS, stamped **only when `multiline`** (previously stamped, meaninglessly, on every field — audit §6.6 P2). Values mirror `.lineLimit(reservesSpace:)` / CSS `field-sizing`, not the `ControlSize` scale.","relationship":"direct"},{"swiftUI":"— (web layout axis, no SwiftUI source)","react":"`distribution?: \"fill\" | \"fit\"`","notes":"Root layout: `fill` (default) fills the container width; `fit` hugs its content. Renamed from `display`/`data-display` (charter §5): the fill/hug axis has exactly one name across the catalog, `distribution`, with `NSSegmentedControl.Distribution` pedigree — `display` collided with the CSS property and had none. Resolved via `data-distribution` in CSS.","relationship":"renamed"},{"swiftUI":"`.disabled(true)`","react":"`disabled?: boolean`","notes":"Inherited from `ControlProps`; uses native `disabled` attribute on the inner element (Shape N, charter §3).","relationship":"direct"},{"swiftUI":"— (native form passthroughs)","react":"`autoComplete?`, `inputMode?`, `readOnly?`, `maxLength?`, `minLength?`","notes":"React-DOM camelCase native passthroughs (audit §6.6 P1.6 — previously absent). `readOnly` hard-suppresses `onChange` in the component itself (charter §3), not only via the native attribute.","relationship":"direct"},{"swiftUI":"— (low-level native passthrough)","react":"`onFocus?(event)`, `onBlur?(event)`","notes":"\"Low-level native passthrough\" (charter §1 rule 2 wording), typed with the branch's concrete `FocusEvent<HTMLInputElement | HTMLTextAreaElement>`. Never fire while `disabled`.","relationship":"direct"},{"swiftUI":"`name` / `id` binding, form participation","react":"`name?`, `id?`, `required?`","notes":"Native form field name / explicit id (falls back to a generated stable id) / native `required` + `aria-required` (A16).","relationship":"direct"},{"swiftUI":"— (validation state)","react":"`invalid?: boolean` + `aria-invalid?`","notes":"`invalid` sets `aria-invalid=\"true\"`. An explicit `aria-invalid` on the same element always wins — including `\"grammar\"` / `\"spelling\"` / explicit `false` — via `ariaInvalid ?? (invalid || undefined)`; `invalid=true` no longer clobbers a caller-supplied richer value (audit §6.6 P2 CONFIRMED).","relationship":"direct"},{"swiftUI":"— (surface override)","react":"`className?`","notes":"The overridable glass-surface class (ADR-0004/0016).","relationship":"direct"},{"swiftUI":"`.tint(color)`","react":"`tint?: LiquidTintValue`","notes":"Recolours caret + selection highlight only — not the shell fill/border. The resting field is tint-independent. Consumed (charter: a `.tint(_:)` analogue must be truthful).","relationship":"direct"},{"swiftUI":"Accessible name (SwiftUI label / prompt)","react":"`aria-label` / `aria-labelledby`","notes":"The demo `LabeledRow` captions are demo chrome; the field's accessible name must be supplied by the caller.","relationship":"direct"},{"swiftUI":"— (error association)","react":"`aria-describedby?` / `aria-errormessage?`","notes":"Forwarded to the native element for error-message association.","relationship":"direct"}],"tokens":["--lq-color-accent","--lq-color-bg-primary","--lq-color-fill-primary-dark","--lq-color-fill-primary-light","--lq-color-label-primary-dark","--lq-color-label-primary-light","--lq-color-label-secondary-dark","--lq-color-label-secondary-light","--lq-color-separator","--lq-color-system-blue-default-dark","--lq-color-system-blue-default-light","--lq-duration-","--lq-easing-","--lq-font-family-sans","--lq-font-size-body","--lq-glass-","--lq-hit-target-touch","--lq-lens-","--lq-letter-spacing-body","--lq-line-height-body","--lq-radius-md","--lq-radius-pill","--lq-space-","--lq-space-16","--lq-text-field-pad","--lq-text-field-slot-gap"],"keyboard":[{"key":"Keyboard","action":"Keyboard: the native / element provides all standard text-editing keys: character entry, ←/→/↑/↓, Home/End, selection with Shift, Cmd/Ctrl+A/C/V/X, Backspace/Delete. Tab moves focus in/out. Multiline Enter inserts a newline (textarea default); single-line Enter submits the enclosing form (input default) and fires onSubmit(value) — the component never calls preventDefault, so native form submission is untouched. Escape on a clearable field with a non-empty value clears it (same path as the clear button: onChange(\"\") then onClear(), focus retained) and calls preventDefault so the key does not also dismiss an enclosing overlay; on an empty or non-clearable field Escape is left entirely to the platform. Both are suppressed while disabled or readOnly. A caller's onKeyDown runs first and can veto either by calling preventDefault."}],"variants":["automatic","plain","prominent","rounded"],"variantPages":[]}