Skip to content

Input — spec

Status: example artifact for component-spec-writer skill Generated by: skills/component-spec-writer, citing Ant Design Input, MUI TextField, shadcn-ui input + label

Purpose

A single-line text input. The most-used form control. Must compose with labels, help text, and errors; must accept controlled and uncontrolled values; must clear WCAG AA in every state.

Anatomy

Field (the row)
├── Label                  (above input)
├── Input
│   ├── [iconStart]?       prefix slot
│   ├── input element
│   ├── [iconEnd]?         suffix slot (status icon, clear button, password toggle, etc.)
│   └── [addonStart/End]?  text/button addon (e.g., `https://`, `/page`)
├── HelpText               (below — neutral hint)
└── ErrorText              (below — replaces HelpText in error state)
Part Purpose Required
Label What this field is yes (visible OR aria-label)
Input The control yes
HelpText Why or how (constraints, format) optional
ErrorText What's wrong, how to fix required when error
iconStart/End Icon affordance inside the field optional
addonStart/End Text or control attached to the side optional

API

Prop Type Default Description
value string Controlled value.
defaultValue string Uncontrolled initial value.
onChange (value: string, e: ChangeEvent) => void Fires per keystroke. First arg is value (most-used), event second.
onBlur (e: FocusEvent) => void Fires when input loses focus. Use to trigger validation.
label string \| ReactNode Visible label. If omitted, aria-label is required.
placeholder string Format hint. Never the label. Disappears on input.
helpText string \| ReactNode Below-field hint. Always visible until error replaces it.
errorText string Below-field error. Setting non-empty implies error={true}.
error boolean inferred from errorText Force error styling without text.
required boolean false Marks visually + sets aria-required. Visible label gets (optional) on others.
disabled boolean false Removes interactivity.
readOnly boolean false Value visible but not editable. Different from disabled — focus/copy still work.
size "sm" \| "md" \| "lg" "md"
type "text" \| "email" \| "password" \| "tel" \| "url" \| "search" "text" Drives mobile keyboard via native attr.
inputMode HTMLAttributes['inputMode'] derived from type Override mobile keyboard.
autoComplete string "email", "new-password", "current-password", "tel", etc. Always set for known semantic fields.
iconStart ReactNode Icon inside left side of input.
iconEnd ReactNode Icon inside right side. Auto-rendered for password toggle, clear button, status.
addonStart ReactNode Attached element outside the input on the left (e.g., https://).
addonEnd ReactNode Attached element outside the input on the right.
clearable boolean false Renders an × clear button when value is non-empty.
showPasswordToggle boolean true for type="password" Eye icon to show/hide.
maxLength number Hard limit. If set, render counter 0/100.
name string Form name. Required for native form submission.
id string auto-generated Used by <label htmlFor>.
fullWidth boolean true Inputs default full-width within their parent.

States

State Trigger Visual
Default resting 1px border --color-border-default, bg --color-bg-default
Hover mouse-over border --color-border-strong
Focus-visible keyboard tab 2px ring --color-focus-ring, 2px offset; border --color-primary-default
Filled value non-empty unchanged (don't darken)
Error error border --color-error, focus ring --color-error/40, errorText shown
Disabled disabled bg --color-bg-subtle, text --color-text-disabled, no border emphasis
Read-only readOnly bg --color-bg-subtle, no border emphasis, cursor text
Loading (async validation) loading (custom prop) spinner in iconEnd slot

Sizes

Size Height Font Padding-x Icon size Use
sm 32px 13px 8px 14px dense forms, compact UIs
md 40px 14px 12px 16px default
lg 48px 16px 16px 20px mobile-priority forms

Tokens consumed

--color-border-default
--color-border-strong
--color-bg-default
--color-bg-subtle
--color-text-primary
--color-text-secondary
--color-text-tertiary
--color-text-disabled
--color-primary-default
--color-error
--color-focus-ring
--space-sm
--space-md
--radius-md
--font-size-sm
--font-size-base
--font-size-lg
--transition-fast

Accessibility

  • Semantic: native <input>. Always paired with <label> (visible) or aria-label.
  • <label htmlFor={id}> is the strongest pattern — clicking the label focuses the input.
  • Required:
  • required attribute (HTML).
  • aria-required="true" (explicit for AT).
  • Visible "Required" indicator (or (optional) on optionals — see knowledge/patterns/form-design.md).
  • Error:
  • aria-invalid="true" when error.
  • aria-describedby pointing to the error text element's id.
  • Error text in role="alert" for assertive announcement, or aria-live="polite" for non-blocking.
  • Help text: aria-describedby pointing to the help element (in addition to error when present, both ids).
  • Password show/hide: the toggle button needs its own aria-label="Show password" / "Hide password".
  • Clear button: aria-label="Clear".
  • Touch target: input height ≥ 40px for mobile. sm (32px) is too small for touch primary forms.
  • Keyboard:
  • Tab reaches.
  • Esc clears search-type inputs (convention, not native).
  • For comboboxes built on top of input, follow WAI-ARIA APG combobox pattern.
  • Autofill: autoComplete attribute is required for password manager and browser autofill. Skipping it is a real friction cost.

Code example

// Default — labeled, with help text
<Input
  label="Email"
  type="email"
  placeholder="name@company.com"
  helpText="We'll only use this for password recovery."
  autoComplete="email"
  value={email}
  onChange={setEmail}
  required
/>

// Error state
<Input
  label="Password"
  type="password"
  value={password}
  onChange={setPassword}
  errorText="Password must include a number."
  autoComplete="new-password"
  required
/>

// With icon and clearable
<Input
  label="Search"
  type="search"
  iconStart={<SearchIcon />}
  clearable
  value={query}
  onChange={setQuery}
/>

// With addon (URL prefix)
<Input
  label="Site URL"
  addonStart="https://"
  placeholder="example.com"
  value={url}
  onChange={setUrl}
/>

// Read-only display
<Input label="Account ID" value="acc_8f2k...3jq" readOnly />

Edge cases

  • Pasting overflows maxLength: native truncates. Surface the behavior with the counter going red and a help-text update.
  • type="number" quirks: avoid for currency/decimals — strips leading zeros, rejects formatting, scrolls on accidental wheel. Use type="text" inputMode="decimal" and parse on blur.
  • Password manager covering iconEnd: 1Password and similar overlay over the right side of password fields. Don't put critical UI there for type="password".
  • iOS auto-zoom on font-size < 16px: focusing an input with smaller font triggers Safari zoom. Either set font-size: 16px or add <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> (the latter is debated for a11y).
  • RTL: iconStart mirrors right, iconEnd mirrors left, padding swaps. Use logical CSS properties.
  • Korean input: IME (input method editor) generates intermediate states (조합 중). Don't validate per-keystroke during composition. Listen to compositionstart/compositionend and skip validation between them.

Don't

  • Don't use placeholder as the only label.
  • Don't make label the only error indicator (color alone fails contrast for color-blind users — always have errorText).
  • Don't disable submit until "valid" without showing why each field is invalid.
  • Don't validate on every keystroke before user has had a chance to finish typing — validate on blur for the first error, then on change after.
  • Don't mask password length with fixed-width dots — show actual length in the bullet count.
  • Don't put iconStart and addonStart together — pick one to indicate the leading affordance.

References

API choices made: - label/helpText/errorText as props rather than separate components: 80% of inputs need all three; props keep usage compact. Composition (<Field><Label/>...) for the 20% that need finer control. - onChange(value, e) ordering: value-first since that's what 95% of consumers want. Event is escape hatch. - autoComplete is documented as critical: ant-design and shadcn don't emphasize it; skipping is a real loss for forms. - clearable and showPasswordToggle as props: ant-design has them, MUI doesn't. They're worth the surface area — every product UI re-implements them otherwise.

Cross-reference