Component catalog
Browse the public controls by interaction purpose. Every result links to generated API facts, accessibility behavior, source, and exhaustive stories.
Browse the public controls by interaction purpose. Every result links to generated API facts, accessibility behavior, source, and exhaustive stories.
A user-editable control for entering freeform text — the canonical glass input field.
import { TextField } from "@liquidify/react/text-field"| Prop | Type | Default | Description |
|---|---|---|---|
| autoComplete | string | undefined | — | Native `autocomplete` passthrough. |
| className | string | undefined | — | 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. |
| clearable | boolean | undefined | false | 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. |
| clearLabel | string | undefined | Clear text | Accessible name of the clear button. |
| defaultValue | string | undefined | "" | Uncontrolled seed for the internal value (ignored once {@link TextSurfaceProps.value} is supplied). Read once, on mount. |
| disabled | boolean | undefined | — | 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. |
| distribution | "fill" | "fit" | undefined | fill | 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`. |
| icon | ReactNode | — | 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. |
| id | string | undefined | — | 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). |
| inputMode | TextInputMode | undefined | — | Native `inputmode` passthrough — hints the on-screen keyboard variant. |
| invalid | boolean | undefined | — | 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. |
| lineLimit | LineLimit | undefined | { min: 2, max: 4 } | 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. |
| maxLength | number | undefined | — | Native `maxlength` passthrough. |
| minLength | number | undefined | — | Native `minlength` passthrough. |
| multiline | boolean | undefined | — | Discriminant: absent/`false` selects the single-line (`<input>`) branch. Discriminant: `true` selects the multiline (`<textarea>`) branch. |
| name | string | undefined | — | Native form field name. When set, the element participates in form submission and React Hook Form can register it (A16). |
| onBlur | ((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | ((event: FocusEvent<HTMLInputElement, Element>) => void) | undefined | — | Low-level native passthrough; fires on native blur of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`. |
| onChange | ((next: string) => void) | undefined | — | 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}. |
| onClear | (() => void) | undefined | — | Fires after the value is cleared via the clear button or `Escape`. |
| onFocus | ((event: FocusEvent<HTMLTextAreaElement, Element>) => void) | ((event: FocusEvent<HTMLInputElement, Element>) => void) | undefined | — | Low-level native passthrough; fires on native focus of the element. Never fires while {@link TextSurfaceProps.disabled} is `true`. |
| onKeyDown | ((event: KeyboardEvent<HTMLInputElement>) => void) | ((event: KeyboardEvent<HTMLTextAreaElement>) => void) | undefined | — | 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}. |
| onSubmit | ((value: string) => void) | undefined | — | 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}. |
| pattern | string | undefined | — | 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}. |
| placeholder | string | undefined | — | Placeholder text shown when the surface is empty — the SwiftUI title-arg analogue (the `prompt` parameter). |
| readOnly | boolean | undefined | — | 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). |
| ref | ((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 | — | Ref forwarded to the native element — the surface's primary interactive element. |
| required | boolean | undefined | — | Marks the surface required — sets the native `required` + `aria-required`. |
| secure | boolean | undefined | false | 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}. |
| sizing | TextFieldSizing | undefined | relative | 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). |
| tint | LiquidTintValue | undefined | — | 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). |
| type | TextFieldType | undefined | text | 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}. |
| value | string | undefined | — | 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}. |
| variant | TextFieldVariant | undefined | rounded | 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. |
TextField supports the documented keyboard interactions below.
| Name | Value |
|---|---|
| --lq-color-accent | #007aff |
| --lq-color-bg-primary | #ffffff |
| --lq-color-fill-primary-dark | Component-defined value |
| --lq-color-fill-primary-light | Component-defined value |
| --lq-color-label-primary-dark | #ffffff |
| --lq-color-label-primary-light | #000000 |
| --lq-color-label-secondary-dark | rgba(235, 235, 245, 0.6) |
| --lq-color-label-secondary-light | rgba(60, 60, 67, 0.6) |
| --lq-color-separator | rgba(60, 60, 67, 0.14) |
| --lq-color-system-blue-default-dark | #0091ff |
| --lq-color-system-blue-default-light | #0088ff |
| --lq-duration- | 0ms80ms120ms150ms250ms350ms500ms |
| --lq-easing- | cubic-bezier(0.25, 0.1, 0.25, 1)cubic-bezier(0, 0, 0.2, 1)cubic-bezier(0.4, 0, 1, 1)cubic-bezier(0.175, 0.885, 0.32, 1.275)cubic-bezier(0.25, 0.46, 0.45, 0.94) |
| --lq-font-family-sans | Inter, Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif |
| --lq-font-size-body | 0.8125rem |
| --lq-glass- | 440.71.15var(--lq-segmented-control-indicator-blur)calc(var(--lq-glass-material-blur) * 8)var(--lq-shadow-presentational) |
| --lq-hit-target-touch | 2.75rem |
| --lq-lens- | Component-defined value |
| --lq-letter-spacing-body | -0.025625rem |
| --lq-line-height-body | 1.25 |
| --lq-radius-md | Component-defined value |
| --lq-radius-pill | 9999rem |
| --lq-space- | 0.0625rem0.125rem0.25rem0.5rem0.75rem1rem1.25rem1.5rem2rem2.5rem3rem3.5rem |
| --lq-space-16 | 1rem |
| --lq-text-field-pad | var(--lq-space-8)var(--lq-space-12) |
| --lq-text-field-slot-gap | var(--lq-space-8) |