Loading…
Loading…
A single-line text field for capturing short-form user input.
The Text Input (also called a text field, input, or text box) is a single-line form control that captures short-form user input. It's the workhorse of web forms — names, emails, passwords, search queries, and countless other data types flow through text inputs.
Despite its apparent simplicity, the text input is deceptively complex. A well-designed text input communicates what it expects (via label, placeholder, and helper text), validates what it receives (inline, in real time), and gracefully handles edge cases (long values, paste, autocomplete, internationalization).
The anatomy of a text input includes up to seven parts: label (above), prefix (leading icon or text), input field (the editable area), suffix (trailing icon or text), helper text (below, for guidance), error text (below, for validation), and character counter (below-right). Not all are visible at once — error text replaces helper text, for example.
When to use a Text Input:
When NOT to use a Text Input:
Preview your input's border-radius with our Border Radius Generator and validate text-to-background contrast with the Contrast Checker.
| Variant | Description | Best For |
|---|---|---|
| Outlined | Full border around the input. Clear boundaries, high discoverability. | Most form contexts. Default choice. |
| Filled | Background fill, no visible border (or bottom-border only). | Material Design style, dense forms. |
| Underlined | Bottom border only. Minimal footprint. | Inline editing, minimal UI. |
| Ghost | No visible boundary until hover/focus. | Inline editable text that looks like static content until clicked. |
HTML type | Purpose | Browser Enhancement |
|---|---|---|
text | Generic text | None (default) |
email | Email addresses | Mobile: shows @ and .com keys. Built-in validation. |
password | Passwords | Masks characters. Password managers detect this. |
tel | Phone numbers | Mobile: shows numeric keypad. |
url | URLs | Mobile: shows / and .com keys. |
search | Search queries | Shows clear button (×) in some browsers. |
number | Numeric values | Shows spinner arrows. Avoid for non-math numbers (phone, ZIP, credit card) — use inputmode="numeric" with type="text" instead. |
| Size | Height | Font Size | Use Case |
|---|---|---|---|
| Small (sm) | 32px | 13px | Dense forms, table inline editing |
| Medium (md) | 40px | 14px | Standard forms |
| Large (lg) | 48px | 16px | Landing page forms, search bars, mobile-first |
Experiment with corner rounding using our Border Radius Generator — 6–8px is standard, pill-shaped (9999px) works well for search inputs.
| Property | Type | Default | Description |
|---|---|---|---|
type | 'text' | 'email' | 'password' | 'tel' | 'url' | 'search' | 'number' | 'text' | HTML input type |
value | string | — | Controlled input value |
defaultValue | string | — | Uncontrolled initial value |
onChange | (e: ChangeEvent<HTMLInputElement>) => void | — | Change handler |
placeholder | string | — | Hint text (disappears on focus or input). Do not use as a label replacement. |
label | string | — | Visible label text. Required for accessibility. |
helperText | string | — | Guidance text below the input |
error | boolean | string | — | Error state and/or message. Replaces helper text when active. |
disabled | boolean | false | Prevents interaction |
readOnly | boolean | false | Prevents editing but allows selection and focus |
required | boolean | false | Marks the field as required |
maxLength | number | — | Maximum character count |
prefix | ReactNode | — | Leading content (icon, currency symbol, "https://") |
suffix | ReactNode | — | Trailing content (icon, unit label, clear button) |
size | 'sm' | 'md' | 'lg' | 'md' | Input height and font size |
autoComplete | string | — | Browser autocomplete hint ("name", "email", etc.) |
| Token Category | Token Example | Text Input Usage |
|---|---|---|
| Color – Background | --color-input-bg | Input field background |
| Color – Border | --color-border, --color-border-focus, --color-border-error | Default, focus, and error border colors |
| Color – Text | --color-text | Input value text |
| Color – Placeholder | --color-text-muted | Placeholder text (must still meet 4.5:1 contrast — often doesn't!) |
| Color – Label | --color-text | Label text above the input |
| Color – Helper | --color-text-muted | Helper text below the input |
| Color – Error | --color-error-600 | Error border, error text, error icon |
| Color – Disabled | --color-text-disabled, --color-surface-disabled | Disabled state colors |
| Spacing | --space-2 (8px), --space-3 (12px) | Internal padding (vertical, horizontal) |
| Border Radius | --radius-md (8px) | Input corners. Preview with Border Radius Generator. |
| Border Width | --border-width (1px) | Default border. Focus border may be 2px. |
| Typography | --font-size-sm, --font-family-body | Input text, label, and helper text |
| Transition | --duration-fast (150ms) | Border color and shadow transition on focus |
See our Design Tokens Complete Guide for a complete token architecture reference.
| State | Visual Change | Behavior |
|---|---|---|
| Default | Border in neutral color, label above, optional helper text below | Ready for input |
| Hover | Border darkens slightly | Indicates interactivity |
| Focus | Border color changes to primary, optional shadow ring appears. | Cursor appears in the field. Helper text remains visible. |
| Filled | Same as default, but with value text visible | User has entered content |
| Disabled | Reduced opacity (0.5), background grayed out | No interaction, aria-disabled="true" |
| Read-only | Normal appearance, cursor changes to default (not text cursor) | Text is selectable but not editable |
| Error | Red border, error icon (optional), error message replaces helper text | Triggered by validation. aria-invalid="true" and aria-describedby pointing to error message. |
| Success | Green border, checkmark icon (optional) | Positive validation feedback (use sparingly — not every valid field needs a green border) |
| Loading | Spinner in the suffix position | Async validation in progress (e.g., checking username availability) |
The focus state is critical — it's the primary indicator for keyboard users. Use a 2px solid ring in a high-contrast color, or combine border color change with a subtle box-shadow:
.text-input:focus-visible {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15);
outline: none;
}
Always verify your focus state meets 3:1 contrast against the surrounding background per WCAG 2.1 SC 1.4.11. Use our Contrast Checker.
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.3.1 Info and Relationships | A | Input must be associated with a <label> via for/id. Programmatic relationship, not just visual proximity. |
| SC 1.3.5 Identify Input Purpose | AA | Use autocomplete attributes ("name", "email", "tel", "street-address") so browsers and assistive tech can autofill correctly. |
| SC 3.3.1 Error Identification | A | When validation fails, identify the field in error and describe the error in text. |
| SC 3.3.2 Labels or Instructions | A | Provide a visible label. Placeholder alone is NOT sufficient — it disappears when the user starts typing. |
| SC 3.3.3 Error Suggestion | AA | If an error is detected and suggestions are known, provide them ("Please enter a valid email, e.g., name@example.com") |
| SC 1.4.11 Non-text Contrast | AA | Input border must have 3:1 contrast against the background. This is commonly violated with light gray borders on white. Test with Contrast Checker. |
| SC 2.4.7 Focus Visible | AA | Focus indicator must be clearly visible. |
aria-required="true" — Marks the field as required. Supplement with a visual indicator (asterisk or "Required" text).aria-invalid="true" — Set when validation fails. Screen readers announce "invalid entry".aria-describedby="helper-id" — Links helper text or error message to the input. Screen readers announce the description after the label.aria-errormessage="error-id" — More specific than aria-describedby for error messages (limited support, use both for now).autocomplete — Not an ARIA attribute, but essential for accessibility. Use values from the HTML spec.| Key | Action |
|---|---|
| Tab | Moves focus into or out of the input |
| Ctrl/Cmd + A | Selects all text |
| Ctrl/Cmd + C/V/X | Copy, paste, cut |
| Escape | May clear search inputs (browser behavior varies) |
| Enter | Submits the parent form if input is within a <form> |
<label>.autocomplete — Without it, browsers can't autofill and password managers can't detect fields.For comprehensive form accessibility, see our Accessible Forms Guide and WCAG Practical Guide.
type. type="email" gives you free validation, appropriate mobile keyboards, and autocomplete hints. Don't default everything to type="text".autocomplete attributes. They're a huge UX win — users fill forms 30% faster with autocomplete. And they're required by WCAG 2.1 SC 1.3.5.inputmode for mobile. inputmode="numeric" shows a number pad on mobile without the downsides of type="number" (spinner arrows, exponential notation, negative values).type="number" for non-mathematical numbers. Phone numbers, credit cards, ZIP codes, and PINs are not quantities. Use type="text" with inputmode="numeric" and pattern="[0-9]*".maxLength limits. Some names are long. Some international addresses are long. Only limit length when there's a real technical constraint (e.g., database column limit).<div class="form-field">
<label for="email-input">Email address</label>
<div class="input-wrapper">
<svg class="input-prefix-icon" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"/>
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"/>
</svg>
<input
type="email"
id="email-input"
name="email"
placeholder="jane@example.com"
autocomplete="email"
required
aria-describedby="email-helper"
/>
</div>
<p id="email-helper" class="helper-text">We'll never share your email with anyone.</p>
</div>
<!-- Error state -->
<div class="form-field form-field-error">
<label for="email-input-err">Email address</label>
<div class="input-wrapper input-error">
<input
type="email"
id="email-input-err"
name="email"
value="not-an-email"
autocomplete="email"
required
aria-invalid="true"
aria-describedby="email-error"
/>
</div>
<p id="email-error" class="error-text" role="alert">
Please enter a valid email address (e.g., name@example.com).
</p>
</div>
<!-- Password with toggle visibility -->
<div class="form-field">
<label for="password-input">Password</label>
<div class="input-wrapper">
<input
type="password"
id="password-input"
name="password"
autocomplete="current-password"
required
minlength="8"
aria-describedby="password-helper"
/>
<button type="button" class="input-suffix-btn" aria-label="Show password">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/>
</svg>
</button>
</div>
<p id="password-helper" class="helper-text">Must be at least 8 characters.</p>
</div>import { forwardRef, useState, useId, type InputHTMLAttributes, type ReactNode } from "react";
interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "prefix"> {
label: string;
helperText?: string;
error?: string | boolean;
prefix?: ReactNode;
suffix?: ReactNode;
size?: "sm" | "md" | "lg";
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
(
{
label,
helperText,
error,
prefix,
suffix,
size = "md",
required,
className = "",
id: externalId,
...props
},
ref,
) => {
const autoId = useId();
const id = externalId ?? autoId;
const helperId = `${id}-helper`;
const errorId = `${id}-error`;
const hasError = Boolean(error);
const errorMessage = typeof error === "string" ? error : undefined;
return (
<div className={`form-field ${hasError ? "form-field-error" : ""}`}>
<label htmlFor={id}>
{label}
{required && <span aria-hidden="true"> *</span>}
</label>
<div className={`input-wrapper input-${size} ${hasError ? "input-error" : ""}`}>
{prefix && <span className="input-prefix" aria-hidden="true">{prefix}</span>}
<input
ref={ref}
id={id}
required={required}
aria-invalid={hasError || undefined}
aria-describedby={
hasError && errorMessage ? errorId : helperText ? helperId : undefined
}
className={className}
{...props}
/>
{suffix && <span className="input-suffix">{suffix}</span>}
</div>
{hasError && errorMessage ? (
<p id={errorId} className="error-text" role="alert">{errorMessage}</p>
) : helperText ? (
<p id={helperId} className="helper-text">{helperText}</p>
) : null}
</div>
);
},
);
TextInput.displayName = "TextInput";
export default TextInput;
// Usage
<TextInput
label="Email address"
type="email"
placeholder="jane@example.com"
autoComplete="email"
helperText="We'll never share your email."
required
/>
<TextInput
label="Password"
type="password"
autoComplete="current-password"
error="Password must be at least 8 characters."
required
/>| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Component | TextField (Outlined / Filled) | Input (styled <input>) | No primitive | Input, Input.Password, Input.Search |
| Floating label | Yes (signature Material animation) | No | N/A | No |
| Prefix/Suffix | Leading/trailing icons | Manual via wrapper | N/A | prefix, suffix, addonBefore, addonAfter |
| Error handling | Supporting text + error color | Manual error state | N/A | status="error" with Form integration |
| Character counter | Built-in with maxLength | Manual | N/A | showCount prop |
| Password toggle | Not built-in | Not built-in | N/A | Input.Password with toggle |
| Search variant | Not built-in | Not built-in | N/A | Input.Search with search button |
| Sizes | Density via tokens | Not built-in | N/A | large, middle, small |
| Form integration | Material form field wrapper | React Hook Form compatible | N/A | Deep integration with Form component |
Material 3's floating label is the most distinctive text input pattern in modern UI design. The label starts inside the input (like a placeholder) and animates up to a floating position above the field when focused or filled. It's space-efficient and elegant, but has accessibility challenges — some screen readers struggle with the animated label, and it limits the label to short text that fits within the field width.
Ant Design offers the most feature-complete input family. Input.Password includes a built-in show/hide toggle. Input.Search adds a search button with enter-to-submit. Input.TextArea has autoSize that grows vertically as content increases. Input.Group combines multiple inputs into a compound field (e.g., phone area code + number).
Shadcn/ui provides a bare Input component — essentially a styled <input> with Tailwind classes. Labels, helper text, and error states are composed manually using their Label and FormMessage components. This modularity is flexible but requires more assembly per form field.
HTML5 input types deserve emphasis: type="email", type="tel", and type="url" give you free mobile keyboard optimization and basic validation with zero JavaScript. Many custom input implementations ironically ship JavaScript to replicate behavior that native HTML provides for free.
The inputmode attribute is an underused gem. inputmode="numeric" shows a number-only keyboard on mobile without the quirks of type="number" (spinner arrows, exponential notation, empty-on-invalid behavior). Use it for phone numbers, credit cards, PINs, and ZIP codes.