Skip to content

Text Input

A single-line text field for capturing short-form user input.

  • Input
  • Text Field
  • Text Box

Overview

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:

  • For short, single-line text (names, emails, URLs, phone numbers, search terms)
  • When you need to capture free-form text that doesn't map to a predefined list
  • For numeric input with formatting (currency, phone numbers) — use with input masking

When NOT to use a Text Input:

  • For long-form, multi-line text — use a Textarea
  • For selecting from predefined options — use a Select or Combobox
  • For boolean values — use a Checkbox or Switch
  • For dates — use a Date Picker (even though typed dates are common, pickers prevent format confusion)

Preview your input's border-radius with our Border Radius Generator and validate text-to-background contrast with the Contrast Checker.

Variants

Text Input Variants

VariantDescriptionBest For
OutlinedFull border around the input. Clear boundaries, high discoverability.Most form contexts. Default choice.
FilledBackground fill, no visible border (or bottom-border only).Material Design style, dense forms.
UnderlinedBottom border only. Minimal footprint.Inline editing, minimal UI.
GhostNo visible boundary until hover/focus.Inline editable text that looks like static content until clicked.

Input Types

HTML typePurposeBrowser Enhancement
textGeneric textNone (default)
emailEmail addressesMobile: shows @ and .com keys. Built-in validation.
passwordPasswordsMasks characters. Password managers detect this.
telPhone numbersMobile: shows numeric keypad.
urlURLsMobile: shows / and .com keys.
searchSearch queriesShows clear button (×) in some browsers.
numberNumeric valuesShows spinner arrows. Avoid for non-math numbers (phone, ZIP, credit card) — use inputmode="numeric" with type="text" instead.

Size Variants

SizeHeightFont SizeUse Case
Small (sm)32px13pxDense forms, table inline editing
Medium (md)40px14pxStandard forms
Large (lg)48px16pxLanding 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.

Properties

Text Input Properties

PropertyTypeDefaultDescription
type'text' | 'email' | 'password' | 'tel' | 'url' | 'search' | 'number''text'HTML input type
valuestring—Controlled input value
defaultValuestring—Uncontrolled initial value
onChange(e: ChangeEvent<HTMLInputElement>) => void—Change handler
placeholderstring—Hint text (disappears on focus or input). Do not use as a label replacement.
labelstring—Visible label text. Required for accessibility.
helperTextstring—Guidance text below the input
errorboolean | string—Error state and/or message. Replaces helper text when active.
disabledbooleanfalsePrevents interaction
readOnlybooleanfalsePrevents editing but allows selection and focus
requiredbooleanfalseMarks the field as required
maxLengthnumber—Maximum character count
prefixReactNode—Leading content (icon, currency symbol, "https://")
suffixReactNode—Trailing content (icon, unit label, clear button)
size'sm' | 'md' | 'lg''md'Input height and font size
autoCompletestring—Browser autocomplete hint ("name", "email", etc.)

Token Mappings

Design Token Mappings

Token CategoryToken ExampleText Input Usage
Color – Background--color-input-bgInput field background
Color – Border--color-border, --color-border-focus, --color-border-errorDefault, focus, and error border colors
Color – Text--color-textInput value text
Color – Placeholder--color-text-mutedPlaceholder text (must still meet 4.5:1 contrast — often doesn't!)
Color – Label--color-textLabel text above the input
Color – Helper--color-text-mutedHelper text below the input
Color – Error--color-error-600Error border, error text, error icon
Color – Disabled--color-text-disabled, --color-surface-disabledDisabled 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-bodyInput 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.

States

Text Input States

StateVisual ChangeBehavior
DefaultBorder in neutral color, label above, optional helper text belowReady for input
HoverBorder darkens slightlyIndicates interactivity
FocusBorder color changes to primary, optional shadow ring appears.Cursor appears in the field. Helper text remains visible.
FilledSame as default, but with value text visibleUser has entered content
DisabledReduced opacity (0.5), background grayed outNo interaction, aria-disabled="true"
Read-onlyNormal appearance, cursor changes to default (not text cursor)Text is selectable but not editable
ErrorRed border, error icon (optional), error message replaces helper textTriggered by validation. aria-invalid="true" and aria-describedby pointing to error message.
SuccessGreen border, checkmark icon (optional)Positive validation feedback (use sparingly — not every valid field needs a green border)
LoadingSpinner in the suffix positionAsync validation in progress (e.g., checking username availability)

Focus Ring Best Practices

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.

Accessibility

WCAG Requirements

CriterionLevelRequirement
SC 1.3.1 Info and RelationshipsAInput must be associated with a <label> via for/id. Programmatic relationship, not just visual proximity.
SC 1.3.5 Identify Input PurposeAAUse autocomplete attributes ("name", "email", "tel", "street-address") so browsers and assistive tech can autofill correctly.
SC 3.3.1 Error IdentificationAWhen validation fails, identify the field in error and describe the error in text.
SC 3.3.2 Labels or InstructionsAProvide a visible label. Placeholder alone is NOT sufficient — it disappears when the user starts typing.
SC 3.3.3 Error SuggestionAAIf 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 ContrastAAInput 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 VisibleAAFocus indicator must be clearly visible.

ARIA Attributes

  • 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.

Keyboard Interaction

KeyAction
TabMoves focus into or out of the input
Ctrl/Cmd + ASelects all text
Ctrl/Cmd + C/V/XCopy, paste, cut
EscapeMay clear search inputs (browser behavior varies)
EnterSubmits the parent form if input is within a <form>

Common Accessibility Mistakes

  1. Placeholder as label — Placeholders disappear, leaving the field unlabeled. Always use a <label>.
  2. Low-contrast placeholder text — Most browsers render placeholders at ~60% opacity. The resulting contrast often fails WCAG. Adjust with CSS.
  3. Red-only error indication — Don't rely on color alone. Add an error icon and text message.
  4. Missing 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.

Usage Guidelines

Do's

  • ✅ Always use a visible label. Even when space is tight, labels are non-negotiable for accessibility and usability.
  • ✅ Use the right type. type="email" gives you free validation, appropriate mobile keyboards, and autocomplete hints. Don't default everything to type="text".
  • ✅ Show error messages inline. Place the error directly below the field it relates to. "Please enter a valid email address" is clear. A generic toast saying "Form has errors" is not.
  • ✅ Provide 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.
  • ✅ Use inputmode for mobile. inputmode="numeric" shows a number pad on mobile without the downsides of type="number" (spinner arrows, exponential notation, negative values).

Don'ts

  • ❌ Don't use 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]*".
  • ❌ Don't validate on every keystroke. It's annoying to see "Invalid email" while you're still typing your email. Validate on blur or on submit. Exception: character counters and format masking can update in real time.
  • ❌ Don't disable paste. Ever. Users paste passwords from password managers. Disabling paste is a security AND usability anti-pattern.
  • ❌ Don't use placeholder text for important instructions. It disappears. Put guidance in helper text below the field instead.
  • ❌ Don't set arbitrary 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).

Content Guidelines

  • Label: Short, descriptive noun — "Email address", "First name", "Search".
  • Placeholder: An example value — "jane@example.com", "e.g., Acme Corp". Not a repeat of the label.
  • Helper text: Formatting guidance — "Must be at least 8 characters", "Include country code".
  • Error text: Specific and actionable — "Email must include @" not "Invalid input".

Code Snippets

html
<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>
tsx
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
/>

Design Systems

Cross-System Comparison

FeatureMaterial 3Shadcn/uiRadixAnt Design
ComponentTextField (Outlined / Filled)Input (styled <input>)No primitiveInput, Input.Password, Input.Search
Floating labelYes (signature Material animation)NoN/ANo
Prefix/SuffixLeading/trailing iconsManual via wrapperN/Aprefix, suffix, addonBefore, addonAfter
Error handlingSupporting text + error colorManual error stateN/Astatus="error" with Form integration
Character counterBuilt-in with maxLengthManualN/AshowCount prop
Password toggleNot built-inNot built-inN/AInput.Password with toggle
Search variantNot built-inNot built-inN/AInput.Search with search button
SizesDensity via tokensNot built-inN/Alarge, middle, small
Form integrationMaterial form field wrapperReact Hook Form compatibleN/ADeep integration with Form component

Notable Approaches

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.

Data InputInputText FieldText Box