Skip to main content

Command Palette

Search for a command to run...

Building Forms That Don't Make Users Hate You

Most form UX complaints aren't about validation logic being wrong — they're about validation feedback happening at the wrong moment.

Updated
4 min readView as Markdown
 Building Forms That Don't Make Users Hate You
J
Jayesh Sojitra | AI & Frontend

Forms get a disproportionate amount of user frustration for how "simple" they seem technically. The actual problem is rarely the validation rules themselves — it's when and how those rules get communicated back to the user.

Mistake 1: Validating on every keystroke, from the first character

// Frustrating: error appears while the user is still typing their email
function EmailField() {
  const [email, setEmail] = useState('');
  const isValid = /\S+@\S+\.\S+/.test(email);
  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {!isValid && <span>Invalid email</span>} {/* shows immediately, even with 1 character typed */}
    </>
  );
}

// Better: validate on blur (when the user leaves the field), then re-validate live once an error exists
function EmailField() {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);
  const isValid = /\S+@\S+\.\S+/.test(email);
  const showError = touched && !isValid;
  return (
    <>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => setTouched(true)}
      />
      {showError && <span>Invalid email</span>}
    </>
  );
}

The fix: don't show an error until the user has had a real chance to finish typing (on blur, not on every change). Once an error is showing, though, switch to live validation on every keystroke — so the user sees it clear the moment they fix it, instead of having to click away again.

Mistake 2: Generic error messages that don't say what's actually wrong

// Unhelpful
<span>Invalid input</span>

// Specific
<span>Password must be at least 8 characters and include a number</span>

"Invalid input" tells the user something is wrong without telling them what to do about it. The specific version tells them exactly what to fix, which is the entire point of the error message existing.

Mistake 3: Clearing the entire form on a failed submission

If a submission fails (validation error, server error), re-rendering the form with everything cleared forces the user to retype fields that were already correct. Preserve the field values, only clear/highlight the specific fields that actually have a problem.

Mistake 4: No loading state during submission

// User can double-click submit, triggering duplicate requests
<button onClick={handleSubmit}>Submit</button>

// Fix: disable + show loading state during the actual request
<button onClick={handleSubmit} disabled={isSubmitting}>
  {isSubmitting ? 'Submitting...' : 'Submit'}
</button>

Without a loading state, users often click submit multiple times when nothing visibly happens right away — which can trigger duplicate submissions if the backend doesn't independently guard against that.

Mistake 5: No indication of which fields are required vs optional

If most fields are required, mark the optional ones ((optional)) rather than marking every required field with an asterisk — fewer visual markers, same information, less clutter. If it's a mix with no clear majority, mark both explicitly.

Mistake 6: Losing user input on an accidental page navigation or refresh

For longer forms specifically (not every form needs this), persisting draft state to localStorage as the user types — and restoring it if they return — prevents losing 10 minutes of typing to an accidental back-button tap or refresh.

useEffect(() => {
  const draft = localStorage.getItem('form-draft');
  if (draft) setFormData(JSON.parse(draft));
}, []);

useEffect(() => {
  localStorage.setItem('form-draft', JSON.stringify(formData));
}, [formData]);

A practical priority order for fixing an existing form

  1. Fix validation timing first (Mistake 1) — this affects every single interaction with the form.
  2. Make error messages specific (Mistake 2) — cheap fix, high impact.
  3. Add a proper loading/disabled state on submit (Mistake 4) — prevents a real, concrete bug (duplicate submissions).
  4. The rest, as time allows — real improvements, but lower frequency of actual user pain than the first three.

Try this yourself: fill out one of your own app's forms deliberately slowly, on purpose making a mistake in one field. Notice exactly when the error appears, what it says, and whether your other correctly-filled fields survive if you have to fix something and resubmit. That's usually the fastest way to find your form's actual pain points.

Takeaway: Good form UX isn't about clever validation logic — it's about respecting the user's time and effort: don't interrupt them before they're done typing, tell them exactly what's wrong, don't make them redo work that was already correct, and show them the app is actually doing something when they submit.

30 Days of AI

Part 2 of 50

A 30-day series breaking down AI concepts, tools, and prompts in plain, jargon-free language — for beginners and professionals who want to actually understand and use AI, not just talk about it.

Up next

Week 3 Recap: Patterns + Debugging

Week 3 is done. Six posts, all circling the same idea — the difference between using a tool and actually understanding it shows up exactly when things get subtle, not when they're simple.