Designer view
Text Input
A bounded surface for free-text user entry that resolves to a single string value — the canonical primitive for forms. Composes a visible label, an `<input>` element, optional description and error message, and optional leading/trailing decorations. Distinct from SearchInput (which emits a query) and from Combobox (which commits from a constrained list) — TextInput accepts and exposes arbitrary user-typed strings.
Also called Text field Input
When to use
Use
For free-text user input where the resulting value is a single string — names, email addresses, URLs, phone numbers, descriptions under a paragraph in length, prices, quantities. The canonical form primitive — most form fields in most products are TextInput.
Avoid
For free-text search queries that route to a results region — that is `SearchInput`. For multi-value input where each value renders as a tag — that is `TagInput`. For selecting a value from a constrained list — that is `Combobox` or `Select`. For long-form prose (multi-paragraph) — that is `Textarea` (separate component, deferred). For dates — a dedicated DatePicker (deferred). For yes/no choices — that is `Checkbox` or `Switch`.
Versus related
- search-input
`SearchInput` emits a free-text query that routes to a separate results region; it has structured submit semantics (Enter, magnifier button, debounced suggestions). `TextInput` accepts and exposes a free-text value; submit behaviour is governed by the parent form. Visually similar; behaviourally distinct — search produces queries, text-input produces values.
- combobox
`Combobox` commits a value from a constrained list (the input value is set to the selected option, not the typed text). `TextInput` accepts arbitrary typed text as the value. Use Combobox when the value must come from a finite known set; use TextInput when any string is acceptable.
- tag-input
`TagInput` accepts multiple discrete values rendered as inline tokens; `TextInput` accepts a single string. Multi-value input (recipients list, tag set, keyword list) is TagInput-shaped; single-value input is TextInput-shaped.
- checkbox
`Checkbox` is a binary form field whose submitted value is the `value` attribute (default `"on"`) only when checked; `TextInput` is a free-text field whose value is the typed string. Both pair with a `<label>` and optional description / error-message slots, but the value type differs — boolean toggle vs string. Use Checkbox for boolean fields; TextInput for typed strings.
- textarea
`Textarea` is a multi-line `<textarea>` whose value carries line-breaks; `TextInput` is single-line `<input type="text">` whose value never contains line breaks. The axes diverge: textarea ships `rows`, `resize`, and auto-resize axes; text-input does not. Both share the label-association rules and the Constraint Validation API. Decision test: does the value carry meaningful line-breaks (Textarea) or is it a single-line value (TextInput)?
Text Input is the canonical form primitive — a labeled string-typed entry surface. Many composite components (Search Input, Combobox, Tag Input, conventional Number Input and Password Input sub-types) layer on top of it; the canonical anatomy here documents the universal field contract. The three visual variants (outlined, filled, underlined) represent industry convergence — Material 3, Carbon, and Polaris each pick a default. The reference covers the label-not-placeholder rule, the aria-invalid cross-modal contract, the required-attribute pairing, the autocomplete semantic intent, and the progressive-validation cadence.
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Auto-layout vertical frame; label on top, input + decorations in the middle, supporting text below |
label | text | Text node above the input; required indicator as a separate text or boolean property |
input | instance | Input field component instance; size and variant drive padding and border treatment |
description | text | Text node below the input; visibility bound to "has description" property |
error-message | text | Text node below the input; visibility bound to invalid state; warm-accent foreground |
leading-icon | instance | Icon component instance; aligned to inline-start; visibility bound to "has leading icon" property |
trailing-icon | instance | Icon component instance; aligned to inline-end; visibility bound to "has trailing icon" property |
Token usage per slot
root- spacing
- gap
spacing.tight
- gap
- color
- ring
color.border.focus
- ring
label- color
- foreground
color.text.primary
- foreground
- typography
- size
text.sm - weight
weight.medium
- size
input- spacing
- padding
spacing.compact
- padding
- radius
- corner
radius.md
- corner
- color
- background
color.surface.bg - foreground
color.text.primary - border
color.border.strong - ring
color.border.focus
- background
- typography
- size
text.md
- size
description- color
- foreground
color.text.muted
- foreground
- typography
- size
text.xs
- size
error-message- color
- foreground
color.text.danger
- foreground
- typography
- size
text.xs - weight
weight.medium
- size
leading-icon- color
- foreground
color.text.muted
- foreground
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | outlined / filled / underlined. |
Size | Enum | size | sm / md / lg. |
Type | Enum | type | text / email / tel / url / password / number. Drives both visual treatment (password masking, number-keyboard hint on mobile) and HTML attribute. |
Label | Text | children of `<label>` | The visible label text. Required slot — never omit, hide visually if needed. |
Placeholder | Text | placeholder | Optional example value. Reserve for "what does a value look like" hints, not for "what is this field" labelling. |
Description | Text | description | Optional supporting text below the input. |
Error Message | Text | errorMessage | Visibility bound to invalid state. |
Has Leading Icon | Boolean | hasLeadingIcon | — |
Has Trailing Icon | Boolean | hasTrailingIcon | — |
Required | Boolean | required | Pairs visual asterisk with the `required` attribute. Both must ship together. |
Disabled | Boolean | disabled | — |
Readonly | Boolean | readonly | Prefer over `disabled` when the value is locked but should remain readable / tabbable. |
Invalid | Boolean | invalid | Pairs `aria-invalid="true"` with the visual error treatment. Wired to error message visibility. |
Leading Icon | Slot | leadingIcon | Decorative — domain hint glyph (envelope, lock, currency). |
Trailing Icon | Slot | trailingIcon | Decorative or interactive depending on variant — document explicitly. |
Motion
| Transition | Duration token |
|---|---|
focusRing | motion.duration.fast |
errorAppear | motion.duration.fast |
labelFloat | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | At and below, inputs typically expand to the full inline-size of their form column — single-column form layouts on mobile are canonical. Multi-column form rows collapse to single-column. Label remains above the input (never shifts to inline-start, which only works at desktop widths). |
breakpoint.md | Above this width, inputs may render at their authored inline-size (typical: medium fields constrain to ~280-360px to discourage long inputs in a fixed-width column). Form layouts may shift to inline labels (label at inline-start, input at inline-end) where horizontal space allows. |
Internationalisation
RTL · mirroring
Label remains above the input (block-level position is direction-neutral). Leading icon position moves from inline-start to inline-start — visually right in RTL, logically still "before the input". Trailing icon mirror-flips to inline-end. Caret follows document direction; mixed-direction values (typing Hebrew into a Latin-default field) honour the input's own `dir` attribute, which can be set per-input. Number-type inputs render numerals in document direction; the input itself accepts both arabic and latin numerals where supported.
Text expansion
Label text expands ~30-50% in long-text languages — "Email" → "E-Mail-Adresse", "Correo electrónico", "البريد الإلكتروني". Allow label inline-size to grow; never truncate label text. Description and error message similarly expand; allow them to wrap to multiple lines. Placeholder text faces the most aggressive expansion ("Enter your email" → 1.5-2x in some languages); inline-size of the input itself often constrains placeholder visibility — if placeholder is critical, ensure the input width accommodates the longest translation.
Variants, properties, states
Variants
Structurally different versions of the component.
outlined filled underlined Properties
The same component, parameterised.
| Property | Type |
|---|---|
type | text | email | tel | url | password | number |
size | sm | md | lg |
invalid | boolean |
disabled | boolean |
readonly | boolean |
required | boolean |
hasLeadingIcon | boolean |
hasTrailingIcon | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | emptyfilledinvalid |
State transitions
| From | To | Trigger |
|---|---|---|
empty | filled | User types into the input. Required-state error (when `required: true`) clears once the field is non-empty, assuming no other validation rules fail. |
filled | empty | User deletes all characters or programmatic clear. For `required: true` fields, transitions back to invalid on blur if validation runs on blur. |
filled | invalid | Validation fires (typically on blur or on form-submit attempt) and the value violates a constraint — `pattern` mismatch, `type="email"` malformed, `minlength` / `maxlength` boundary, or custom `setCustomValidity()` rule. Error message renders; `aria-invalid="true"` set on the input. |
invalid | filled | User edits the value to satisfy the violated rule. Validation re-runs (immediately if validating on-input, on next blur if validating on-blur); `aria-invalid` clears; error message hides. |
Figma↔Code mismatches
- 01 Figma
TextInput drawn with placeholder text used as the label
CodeA field with a real `<label for>` separate from the placeholder
ConsequenceDesigners sometimes treat the placeholder as the label for visual minimalism. The implementation either follows the design (no real label, accessibility breaks; SR users get no field name once the user starts typing) or diverges from the design (adds a label that the design doesn't show). Either way the canonical contract is broken.
CorrectAlways render a real `<label>` and associate it with the input. If the visual design wants no visible label, hide the label with a visually-hidden utility (clip-path / sr-only) — the label remains in the accessibility tree. The placeholder is a hint about the *value* (an example), not the field's identity. Document the canonical: label always present in the markup; `placeholder` reserved for example values, never for "what is this field".
- 02 Figma
Required indicator drawn as a decorative asterisk next to the label
CodeA field with the `required` attribute on the input
ConsequenceDesigners add a red asterisk in the label as a visual cue. Implementations either copy the asterisk and forget the `required` attribute (form submits with empty values; browser-native validation never fires) or set `required` and forget the asterisk (the constraint exists but the user has no visual signal until validation fires). The visual signal and the constraint diverge.
CorrectBoth must be present together. The asterisk (or "(required)" text) is decorative and visible; the `required` attribute on the input is the actual constraint and surfaces in browser validation, in `aria-required`, and in form-submission semantics. Document the canonical contract: visual indicator + attribute always paired.
- 03 Figma
Error message drawn as red text below the input
CodeAn error message rendered via `aria-describedby` plus `aria-invalid`
ConsequenceDesigners draw red error text. Implementations following the design at the visual layer ship error text that has no programmatic relationship to the input — SR users hear the input's label and type, but never hear the error reason. Visual users see "Email address — Invalid format"; SR users hear "Email address, edit text" with no clue why the field is rejected.
CorrectThe error message node has an `id`; the input's `aria-describedby` lists that id (alongside any persistent description); the input's `aria-invalid="true"` flags the error state. The error node should be `role="alert"` or `aria-live="polite"` so dynamic validation announces. Document: visual error + ARIA wiring + invalid attribute, always together.
- 04 Figma
Input variants drawn with different layouts (outlined vs filled vs underlined)
CodeA single `<input>` primitive with variant-driven styling
ConsequenceDesigners compose the three variants as separate Figma components with subtly different anatomies (filled has no border but has a tinted background; underlined has only a bottom border; outlined has all four borders). Developers implement them as a single component with a `variant` prop, and the slot positions diverge — the leading icon sits slightly differently across the three.
CorrectThe canonical anatomy is identical across variants — same slots, same positions, same z-order. Variant differences live in the styling (border, background, padding within the input field) only. Document the variant axis as "visual treatment" not "structural restructuring"; anatomy is stable.
- 05 Figma
Disabled state drawn with low-contrast text and no other affordance
CodeAn input with the `disabled` attribute
ConsequenceDesigners grey out the disabled state to ~40% opacity for visual clarity. Without further care, the contrast ratio drops below WCAG 1.4.3 (4.5:1) and SR users hear "edit text, dimmed" but the canonical wording for "this field is currently not editable, here's why" is missing.
CorrectDisabled state must still meet 3:1 minimum contrast (WCAG 1.4.11 non-text contrast for the input border). Where possible, prefer `readonly` over `disabled` — readonly keeps the value tabbable and copyable, communicates "value is locked" rather than "field is unavailable", and SR announces "edit text, read only" which is more informative than "disabled". Use `disabled` only when the field is genuinely inactive in the current context.
Contracts
Non-negotiable contracts
HTML specHTML label element + WCAG 1.3.1 / 3.3.2 A `<label for="<input-id>">` element exists in the DOM for every field. Visually-hidden via sr-only is allowed; replacement with `placeholder` is not. Placeholder is reserved for example values, not for field identity.
Placeholder-as-label vanishes the field's identity once the user starts typing; SR users never hear it (most AT does not announce placeholder as accessible name); users tabbing away and back lose orientation. Research consistently shows placeholder reduces successful form completion.
APGAPG: Form patterns + WCAG 3.3.1 (Error Identification) When the field is in error: `aria-invalid="true"` on the input, `aria-describedby` referencing the error message node, and the error node carries `role="alert"` (or `aria-live="polite"` for async announcements). Visual error + ARIA error + invalid attribute ship together.
Red border without `aria-invalid` is sighted-only; SR users perceive a normal field. The cross-modal contract — every visual cue has an AT cue — is the foundation of accessible forms.
HTML specHTML required attribute + WCAG 3.3.2 (Labels or Instructions) Visible required indicators (asterisk, "(required)" suffix) are paired with the `required` attribute on the input. The HTML attribute implies `aria-required="true"` and triggers browser-native constraint validation.
Visual-only required loses browser validation, leaves SR users unaware the field is mandatory, and shifts all empty-required detection to the server. Worse: SR may pronounce the asterisk as "asterisk" without any required-state announcement.
HTML specHTML autocomplete attribute (WHATWG) + WCAG 1.3.5 (Identify Input Purpose) `autocomplete` is set to the field's semantic intent (`email`, `username`, `current-password`, `new-password`, `street-address`, `tel`, `cc-number`, etc.). Sitewide `autocomplete="off"` is reserved for genuinely autofill-hostile fields (one-time codes, captcha, security-question answers).
Disabling autofill sitewide breaks password managers and platform autofill; user experience degrades for the 80%+ of users who rely on autofill. Worse, WCAG 1.3.5 explicitly requires `autocomplete` for canonical personal-data fields at AA conformance.
Canon Validation cadence is progressive: validate on blur (user finishes the field), clear errors on input (user corrects), surface errors at submit only as a fallback for fields the user skipped. Browser-native constraint validation (`type="email"`, `pattern`, `min`/`max`) handles format checks at the right moment.
Submit-only validation produces forms where users get bounced back to a field they typed minutes earlier; long forms become exhausting; abandonment rates rise. Inline-on-input validation overcorrects in the other direction by yelling at users mid-typing.
Vocabulary drift
- Material 3
Text fields (filled / outlined)- Material 3 picks `filled` as default with `outlined` as the alternate; canonical reference documents both plus `underlined` because every variant ships in production somewhere.
- Carbon
Text input- Carbon picks `outlined` as default; field-level density and size axes match the canonical anatomy.
- Polaris
TextField- Polaris picks `filled` as default with explicit helper-text and error patterns matching the canonical description / error-message slots.
- React Aria
TextField- React Aria's `useTextField` hook provides the canonical ARIA wiring (label, input, description, errorMessage) as a single primitive matching this canon.
- Headless UI
Input + Field + Label + Description + ErrorMessage- Headless UI does not ship a single `TextInput` primitive; consumers compose `<Field>` plus `<Input>` plus surrounding `<Label>`, `<Description>`, `<ErrorMessage>`. Same composite contract, different granularity.
Common mistakes
#placeholder-as-label
Placeholder used as the only label
The input has no `<label>`; the field's identity lives in the placeholder, which disappears once the user starts typing. Users who tab away and back lose track of what the field was for; SR users never hear the label at all because placeholder is not part of the accessible name in most assistive-tech behaviour.
Always render a `<label for="<input-id>">`. Hide it visually with a sr-only utility if the design wants no visible label, but never remove it from the DOM. Reserve `placeholder` for example values ("name@example.com"), and consider omitting placeholder entirely — research consistently shows placeholder text reduces successful form completion.
#missing-aria-invalid
Error styling without `aria-invalid`
The field shows a red border and an error message visually, but the input lacks `aria-invalid="true"`. SR users hear the input as a normal field; the error message may or may not be announced depending on whether `aria-describedby` is wired. The visual signal is not cross-modal.
Set `aria-invalid="true"` on the input whenever the field is in error. Wire `aria-describedby` to the error message node. Make the error node `role="alert"` or `aria-live="polite"` for asynchronous announcements. Visual-error + ARIA-error + invalid-attribute ship together.
#required-without-attribute
Visible required indicator without the `required` attribute
The label shows a red asterisk; the input lacks the `required` attribute. The user submits the form with the field empty; browser-native validation never fires; server-side validation has to catch what should have been a client-side rejection. Worse: SR users hear the label with the asterisk pronounced as "asterisk" but the field is not announced as required.
Pair the visual asterisk with the `required` attribute on the input. Browser validation surfaces the empty-required message; `aria-required="true"` (or the `required` attribute itself, which implies aria-required) tells SR the field is mandatory. The visual and the constraint always together.
#wrong-autocomplete
`autocomplete="off"` on every input by default
The implementation disables autocomplete sitewide as a perceived security or UX measure. Password managers stop working; platform autofill cannot pre-fill addresses, emails, names. User experience degrades for the 80%+ of users who rely on autofill for forms.
Set `autocomplete` to the field's semantic intent — `email`, `username`, `current-password`, `new-password`, `street-address`, `tel`, `cc-number`, etc. The full token list is in the HTML spec. Reserve `autocomplete="off"` for fields where autofill is genuinely wrong: one-time codes, captcha responses, security-question answers.
#error-only-on-submit
Errors only surface on form submission attempt
The implementation validates only on form submit. Users type a malformed email, tab away, fill out the rest of the form, hit Submit, and get bounced back to the email field with the error. The friction compounds with each field; long forms become exhausting.
Validate progressively — show errors on blur (when the user finishes the field) for canonical types, and clear errors on input (as the user corrects). For `type="email"` and similar, browser-native constraint validation handles the format check at the right moment. Document the cadence: validate-on-blur, clear- on-input, surface-on-submit-as-fallback.
Used in patterns
- Login Formidentifier field (email or username)
- Login Formpassword field
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
root | No landmark role. The grouping is implicit — accessibility comes from the explicit label-input association (`<label for>` or `aria-labelledby`) and from `aria-describedby` linking the input to the description and error nodes. | |
label | Real `<label for="<input-id>">`. Programmatic association is non-negotiable — clicking the label focuses the input and screen readers announce the label when focus enters. For visually hidden labels, use a visually-hidden utility (`clip-path` / `position: absolute; width: 1px`) — never `display: none` or `visibility: hidden`, which remove the label from the accessibility tree. | |
input | Native `<input type="text">` (or the type matching the input domain) is canonical. Set `aria-invalid="true"` when the field is in error and `aria-describedby` listing the ids of the description and error message nodes. `autocomplete` should match the field's semantic intent (`email`, `current-password`, `street-address`, etc.) so password managers and platform autofill work; bare `autocomplete="off"` is reserved for fields where autofill is genuinely wrong (one-time codes, captcha-adjacent). | |
description | Reference the description node from the input via `aria-describedby="<description-id>"`. Screen readers announce the description after the label and the input's type — the hint is part of the field's spoken contract. For long descriptions consider truncation rules carefully; SR pronounces the full text regardless of visual clipping. | |
error-message | Reference the error node from the input via `aria-describedby` (in addition to the description, when both are present — multiple ids space-separated). The node should also be `role="alert"` or live-region (`aria-live="polite"`) so dynamic validation surfaces are announced when the message appears asynchronously, not only on focus. Pair with `aria-invalid="true"` on the input — visual styling alone is not the announcement. | |
leading-icon | Decorative — `aria-hidden="true"`. The icon is a visual affordance; the field's accessible name still comes from the `<label>`. Never substitute the icon for the label. | |
trailing-icon | For interactive trailing icons (password reveal, clear), wrap in a real `<button type="button">` with explicit accessible name ("Show password", "Clear value"). For decorative trailing icons (status checkmark on valid input), `aria-hidden="true"`. Document the variant choice — a single visual treatment must not silently switch between interactive and decorative modes. |