Input — the contract between user and data

An input looks simple, but in a DS it is one of the most expensive components: label, hint, error, prefixes, sizes, textarea, a11y, and form states.

Surface types

  • Text input — single-line entry (type="text" | email | password | search | …).
  • Textarea — multi-line; same border/focus tokens, different height control.
  • Composite fields — input + addon (search icon, currency suffix). Prefer composition over dozens of boolean props.

Required Label ↔ Control pairing

Every field needs an accessible name:

<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" autoComplete="email" />

Or a wrapper:

<label>
  Email
  <input name="email" type="email" />
</label>

Placeholder does not replace a label: it disappears on input and is a poor permanent accessible name for screen readers.

Field anatomy

Recommended composition:

  1. Label
  2. Optional: description / hint (id + aria-describedby)
  3. Control (input/textarea)
  4. Optional: error text (role="alert" or linked via aria-describedby)
  5. Optional: character count / helper
function Field({ id, label, hint, error, children }) {
  const hintId = hint ? id + '-hint' : undefined;
  const errorId = error ? id + '-error' : undefined;
  const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      {hint ? <p id={hintId}>{hint}</p> : null}
      {/* children — clone the input with id, aria-invalid, aria-describedby */}
      {error ? <p id={errorId} role="alert">{error}</p> : null}
    </div>
  );
}

Validation states

StateVisualA11y
defaultneutral border
focusring / accent bordervisible focus
filledoptional slightly different background
errorerror-colored border + textaria-invalid="true", error description
disabledmuted + not editabledisabled
readOnlysimilar to disabled, but focus/copy still workreadOnly

Do not rely on error color alone: add an icon and text. Color alone fails for color blindness.

Errors: UX rules

  • Show errors after blur or submit — do not shout at the first character.
  • Error text says how to fix it, not only “Invalid.”
  • Link the message to the field via aria-describedby.
  • With multiple form errors — move focus to the first problem field.

Input / Textarea API

<Input
  id="username"
  label="Username"
  hint="Latin letters, 3–20 characters"
  error={errors.username}
  size="md"
  disabled={false}
  leftIcon={<SearchIcon />}
/>

<TextArea
  id="bio"
  label="About you"
  rows={4}
  maxLength={300}
  error={errors.bio}
/>

Keep a native input/textarea inside and forward the ref (forwardRef) — forms and libraries like React Hook Form will thank you.

Styles via tokens

.input {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  font: var(--font-body-md);
  min-height: 40px;
}
.input:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}
.input[aria-invalid="true"] {
  border-color: var(--color-danger-500);
}

Accessibility checklist

  • There is a label (visible or aria-label in rare compact UIs).
  • The error is linked to the control.
  • Text and placeholder contrast are sufficient (placeholder can be darker than a typical “gray”).
  • autocomplete for common fields (email, name, tel).
  • 200% font zoom does not break the field layout.

Practice

Build Field + Input + TextArea with shared border/focus tokens. Add stories: default, with hint, error, disabled, with icons.