Field Validation States
One field in four designed states, with a live instance whose validation waits until the user has actually said something.
Hector: Solid, honourable, high-craft work.
Exhibit case
Why it matters
Form validation is usually two failures at once: it fires before the user has finished typing, and it signals the result with colour alone. The first makes the interface feel hostile — an email field turning red on the first keystroke is technically accurate and practically rude. The second makes the result invisible to anyone who cannot resolve red from green, which is roughly one in twelve men.
This field treats validation state as a designed set of four — rest, hint, invalid, confirmed — where each state carries a border weight, a glyph, and a sentence, and colour is only the third signal. The live instance demonstrates the timing rule: nothing is asserted until there is something to assert.
Key techniques
- Four states designed as one pass and rendered together, so the border weights step consistently rather than each being chosen in isolation.
- State derived from value, not stored separately — there is no way for the message and the border to disagree.
aria-invalidset only when genuinely invalid, andaria-describedbywiring the message to the input so the reason is announced with the field, not orphaned beside it.- A leading glyph (
✕,✓,·) so the state survives with colour removed entirely. useId()for the input/message pair, which means the component can be rendered in a list without colliding ids.- Empty is
hint, notinvalid. A field the user has not reached yet has done nothing wrong.
What to steal
The transferable part.The three-signal rule. Border weight, glyph, sentence, then colour. If you delete the colour and the state is still legible, the field is correct.
`aria-describedby` on the message. One attribute turns a visually-adjacent error into an announced reason. This is the single most commonly missed line in form accessibility.
Deriving state from value. The moment validation state is its own useState, it will drift out of sync with the value on some path — a reset, a programmatic fill, an undo. Derive it and that class of bug cannot exist.
What not to copy
Context-specific or costly.Do not use the live-on-every-keystroke timing for anything with expensive or ambiguous validation — a username availability check, a password strength rule with many clauses. Live invalidation on a half-typed password reads as nagging. Validate those on blur, and reserve live feedback for rules the user can satisfy monotonically as they type.
Do not carry the confirmed state everywhere. A green tick on every satisfied field turns a form into a scoreboard and dilutes the states that matter. Reserve it for fields where correctness is genuinely non-obvious.
Inclusion rationale
Forms are where craft is most visible to ordinary users and most often absent. This entry earns its place by making the timing question — when do you tell someone they are wrong — as explicit as the visual question, and by demonstrating a state system that survives having its colour taken away.
Technical register
Performance
Validation is a single regular expression per keystroke against a short string; no debounce is needed or used. border-color and box-shadow are the only transitioned properties, both compositor-friendly. The component holds one string of state and renders one element plus an optional paragraph.
Mobile
The input is 48px tall and uses type="text" in the demo for inspectability, but production email fields should set type="email" and inputMode="email" to get the right on-screen keyboard. The message sits below the field rather than beside it, so it never competes for width at 375px and never gets covered by the keyboard the way an above-field message can.
Accessibility
aria-invalid is present only on the invalid state, so assistive technology is not told about a problem that does not exist. aria-describedby links the message so it is read as part of the field. The glyph is aria-hidden because the message it precedes already carries the meaning — announcing "cross" before the reason is noise. Disabled holds 40% opacity, and the destructive and success tokens are both chosen to clear 4.5:1 against the surface, so the colour layer is legible even though it is never load-bearing.
Study next