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