Why Forms Are So Hard to Get Right
Forms look simple. A few inputs, a button, done. But they're actually the hardest UI to build well. Validation, error states, loading states, accessibility, keyboard navigation, multi-step flows, responsive layout. Get any of these wrong and people leave. Here's how to get them right.
Structure First
Validation That Helps
Bad validation yells at people. Good validation guides them:
- Validate on blur, not on type. Don't show errors while someone is still typing.
- Clear errors when they fix it. Remove the error as soon as the input becomes valid.
- Say what's wrong AND how to fix it. Not "Invalid input." Instead: "Email must include @."
- Mark required fields. Either mark required or optional. Not both. Most forms mark optional since most fields are required.
- Use native validation when possible.
type="email",required,patternwork great.
Loading and Success States
Accessibility Checklist
- Every input has a visible
<Label>linked with htmlFor - Error messages are linked to inputs with
aria-describedby - Invalid inputs have
aria-invalid="true" - Required fields are marked with
aria-required="true" - Tab order follows visual order
- Form can be submitted with Enter key
- Focus moves to first error after failed submission
UX Tips
- Single column. Multi-column forms are harder to scan. One column is almost always better.
- Big enough inputs. Minimum height of 40px. 44px is better for touch.
- Autofocus the first field. Save users a click.
- Use the right input type.
type="email"shows the @ keyboard on mobile.type="tel"shows the number pad. - Break long forms into steps. Show progress. Let people go back.
- Save progress. If the form is long, save to localStorage so people don't lose their work.
The Short Version
- Label every input. Use htmlFor. No exceptions.
- Validate on blur, not on type. Clear errors when fixed.
- Show what's wrong and how to fix it
- Use React Hook Form + Zod for validation
- Always show loading and success states on submit
- Single column, big inputs, right input types
- Break long forms into steps with saved progress