Form Interaction Craft
When to Use
Validate on blur for the first time. After the first error, switch to live validation. Never validate on every keystroke for format errors — it destroys UX.
Decision
| Timing | Trigger | Use When |
|---|---|---|
| On blur (after field exit) | focusout event |
Default for most fields — user had a chance to complete input |
| On submit only | submit event |
Short forms, destructive actions, low-stakes data |
| After first blur, then live | focusout → switch to input |
Best UX: don't interrupt typing, fix errors in real-time after first attempt |
| Live from first keystroke | input event |
Password strength indicator only — never for format validation |
| On keystroke with debounce | input + debounce(300ms) |
Username availability check, async validation |
Multi-step form state:
| Approach | Use When |
|---|---|
| Single page, hide/show sections | < 5 steps, all data needed together at submit |
| Separate URL per step | > 5 steps, users may need to bookmark or share a step |
| Wizard with back/next | Complex forms where later steps depend on earlier answers |
Pattern
// Smart validation — validate on blur, then live after first error
function smartValidation(input, validate) {
let hasBlurred = false;
input.addEventListener('blur', () => { hasBlurred = true; showError(input, validate(input.value)); });
input.addEventListener('input', () => { if (hasBlurred) showError(input, validate(input.value)); });
}
// Auto-resize textarea
function autoResize(textarea) {
function resize() { textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; }
textarea.addEventListener('input', resize);
resize();
}
// Autosave with debounce + status feedback
function autosave(form, saveFn) {
const status = form.querySelector('[data-autosave-status]');
const save = debounce(async () => {
status.textContent = 'Saving...';
try { await saveFn(new FormData(form)); status.textContent = 'Saved'; setTimeout(() => { status.textContent = ''; }, 3000); }
catch { status.textContent = 'Save failed — check your connection'; }
}, 1000);
form.addEventListener('input', save);
window.addEventListener('beforeunload', () => saveFn(new FormData(form))); // Safety net
}
// Click-to-edit inline
function inlineEdit(displayEl, editEl, saveFn) {
displayEl.addEventListener('dblclick', () => {
displayEl.hidden = true; editEl.hidden = false; editEl.value = displayEl.textContent; editEl.focus(); editEl.select();
});
async function commit() {
const value = editEl.value.trim();
if (value && value !== displayEl.textContent) { await saveFn(value); displayEl.textContent = value; }
displayEl.hidden = false; editEl.hidden = true;
}
editEl.addEventListener('blur', commit);
editEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); commit(); }
if (e.key === 'Escape') { displayEl.hidden = false; editEl.hidden = true; }
});
}
Error message craft:
| Rule | Example |
|---|---|
| Specific, not generic | "Email must include @" not "Invalid email" |
| Actionable | "Password must be 8+ characters" not "Password too short" |
| Inline under field | Never at top of form only |
| Screen reader announcement | aria-live="polite" region for dynamic errors |
Common Mistakes
- Wrong: Validating on every keystroke → Right: Error flashes immediately before user finishes typing; use blur + debounce
- Wrong: Preventing paste in masked inputs → Right: Users cannot paste formatted phone numbers from contacts; strip and reformat instead
- Wrong: Autosave without status feedback → Right: User doesn't know data is saved; exits without confidence
- Wrong:
onclickfor inline edit trigger → Right: Addkeydown Entertoo — onclick is mouse-only - Wrong: Multi-step form with no back navigation → Right: WCAG 3.3.4 violation; always allow review and correction
- Wrong: Multi-step state in JS variables only → Right: Use
sessionStorageor URL params; page refresh clears JS state
See Also
- Keyboard Navigation Craft — keyboard handling within form widgets
- Debounce and Throttle — debounce for search inputs and async validation
- Optimistic UI — autosave pending state patterns
- Reference: Smashing Magazine: Inline Validation UX