Forms that don't nag
beginner·8 min·Markdown for agents
What this lesson teaches
Validate fields at the right moment, write errors that fix themselves, and wire the dozen small behaviors every form owes its user.
The field below opens mid-word, already scolding you for maya@. Switch to On blur, type maya@ again, then tap the name field.
The keystroke version shows an error after the first letter. You were not wrong; you were not finished. That is the cardinal sin of form validation: punishing someone for an action still in progress.
The cadence
means checking each field where it is, as the person moves through the form, instead of one list of errors after Submit. The timing is the whole craft:
While typing: positive signals only
No red borders, no error text. The only feedback allowed is good news: a small check when the field becomes valid.On blur: the real check
When the person leaves the field (), validate. If it fails, show the error now. They said "I'm done"; tell them if it's wrong.Once an error shows: check on every change
From then on, re-validate on each keystroke so the error clears the moment the value is fixed. Making them leave the field again to find out reads as the form still being cross.On submit: a final pass
Catch what blur missed (rules across fields, server checks). If anything fails, move focus to the first invalid field.
Here it is in a real product: the error lands only when the cursor moves on to Password, and clears as the ending is typed.
What an error looks like
Each failing field gets three things, together: a danger-colored border, a message directly under the field (13 to 14px, in its own lane that exists before the error does, so the form never jumps), and aria-invalid plus aria-describedby on the input so a screen reader reads the message with the field.
Rules that span two fields, like "passwords match" or "start date before end date", are checked when the second field blurs. The first one cannot be wrong on its own.
✕ Before
Invalid input.
Please enter a valid email.
✓ After
Add the domain, like name@company.com
Good error copy says what is wrong and how to fix it, skips "Please", never says "Invalid", and sounds like a person.
When not to validate
- A single search box: just search.
- A comment or chat message: there is no wrong answer.
- An inline rename: validate on Enter, not on blur, since the person may click away with the value intact.
And mark only the fields that are truly required; label the rest "Optional".
Everything else a form does
Validation is one job. A form owes about a dozen more, and every one is easy to forget.
| Behavior | The rule |
|---|---|
| Labels | Every input has a real <label>, so clicking it focuses the field. A placeholder is not a label |
| Input types | type="email", tel, url, password: the right phone keyboard and autofill come free |
| Font size | Inputs at 16px or more on phones, or iOS zooms the page in |
| Enter | Wrap fields in a <form> so Enter submits; a textarea submits on Cmd or Ctrl+Enter |
| Submitting | Disable the button while the request runs and change its label ("Saving…"); re-enable it in finally, or one failure locks the form |
| Failure | A failed submit says so and keeps every value. Never make anyone retype |
| Icons in the field | Positioned over the input with padding reserved, never a sibling the click misses |
| Autofocus | First field in a desktop dialog, never on touch, where it throws the keyboard up |
| Prefill | Anything you already know: the current username, the ?email= from an invite link |
| Accidents | Long forms warn before leaving, or keep a draft, so a stray back-swipe costs nothing |
fetch does not throw on a server error. An agent's try { await fetch() } catch {} treats a 500 as success. Check response.ok, and never call reset() before you know it worked.
Rework this form's validation:
- While typing: no errors; show a small check inside the field once it is valid.
- On blur: validate and show the error under the field in a reserved message lane (no layout shift).
- Once a field has shown an error, re-validate on every change so it clears as soon as it is fixed.
- On submit: run everything, focus the first invalid field. Cross-field rules run when the second field blurs.
- Error copy says what is expected ("Add the domain, like name@company.com"), no "Please", no "Invalid".
- Inputs have <label>, the right type, 16px font, aria-invalid and aria-describedby.
- The submit button disables with "Saving…" while in flight, re-enables in finally; a failed submit keeps every value and checks response.ok.Quick check
When should an email field first show 'Add the domain, like name@company.com'?
In the skill
The nodes your agent reads when it does this work.
- Forms — inline validationValidate at input, not on submit. Inline errors beat error summaries. Soft validation while typing, hard validation on blur.
components/forms-validation - Forms — behaviorThe semantics the platform gives you, then the details — label wiring, right input type and keyboard hints, 16px, focus-visible, autocomplete where it helps, decorations over the input, autofocus only on desktop modals, a real form so Enter submits, buttons that disable while submitting and re-enable in finally, failed submits that keep every value, no dead zones, confirmed destruction, prefilled everything, drafts that survive a refresh.
components/forms-behavior
Connected lessons
What this lesson builds on and leads to, across every course.