Submit states, inline errors
Keep submit enabled, show errors inline, and focus the first one.
Web Interface Guidelinesvercel-labs · command.md
Submit button stays enabled until request starts; spinner during request Errors inline next to fields; focus first error on submit
Web Interface Guidelinesvercel-labs · command.md
Async updates (toasts, validation) need `aria-live="polite"` Error messages include fix/next step, not just problem
From web-design-guidelines (Web Interface Guidelines).
Sign-up form
Enter a name and ada@company, then submit both.
Disabled until validBad
disabled={!isValid}0 dead clicksErrors on submitGood
aria-invalid + focus()focus: noneform.addEventListener("submit", (e) => {
e.preventDefault();
const errors = validate(form);
for (const input of form.elements) {
const msg = errors[input.name];
input.setAttribute("aria-invalid", String(Boolean(msg)));
document.getElementById(`${input.id}-err`).textContent = msg ?? "";
}
const first = [...form.elements].find((el) => errors[el.name]);
if (first) return first.focus();
submit(form);
});
Save, three times fast
Triple-click each Save button.
No request stateBad
Requests: 0Duplicates: 0
Pending guardGood
Requests: 0Clicks ignored: 0
let pending = false;
button.addEventListener("click", async () => {
if (pending) return;
pending = true;
button.setAttribute("aria-busy", "true");
button.setAttribute("aria-disabled", "true");
label.textContent = "Saving…";
try { await save(); live.textContent = "Saved."; }
finally { pending = false; button.removeAttribute("aria-busy"); button.removeAttribute("aria-disabled"); label.textContent = "Save changes"; }
});
Error copy that names the fix
Type in the field and compare the messages.
States the problemBad
Screen reader hearsnothing
<p class="error">States the fixGood
Screen reader hearsnothing yet
aria-live="polite"function passwordError(value) {
const missing = 8 - value.length;
if (missing > 0) return `Password needs 8+ characters. Add ${missing} more.`;
if (!/\d/.test(value)) return "Add a number, like 7, to finish.";
return "";
}
Why it matters, pros and cons
Why it matters
A disabled button says no without saying why; an enabled one names the field.
Pros
- Every click gets an error, spinner or result.
- Focus lands on the field to fix.
- A pending guard stops duplicate writes.
Cons
- A client guard is not idempotency; servers need request keys.
- Never move focus while someone is typing.
- Under ~300 ms, delay the spinner or it flickers.