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:
- Label
- Optional: description / hint (
id+aria-describedby) - Control (input/textarea)
- Optional: error text (
role="alert"or linked viaaria-describedby) - 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
| State | Visual | A11y |
|---|---|---|
| default | neutral border | — |
| focus | ring / accent border | visible focus |
| filled | optional slightly different background | — |
| error | error-colored border + text | aria-invalid="true", error description |
| disabled | muted + not editable | disabled |
| readOnly | similar to disabled, but focus/copy still work | readOnly |
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-labelin rare compact UIs). - The error is linked to the control.
- Text and placeholder contrast are sufficient (placeholder can be darker than a typical “gray”).
autocompletefor 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.