Form Design
Forms are where users do actual work: sign up, check out, submit data, configure settings. A well-designed form gets out of the way. A poorly designed one causes abandonment, errors, and support tickets. Form design has more research behind it than almost any other UX topic.
Form Layout Best Practices
Single-Column Layout
Single-column forms have the highest completion rates. The eye moves straight down without jumping between columns.
BEST (single column): AVOID (multi-column):
┌───────────────────────┐ ┌───────────────────────────┐
│ First name │ │ First name │ Last name │
│ [_______________] │ │ [________] │ [________] │
│ │ │ │
│ Last name │ │ Email │ Phone │
│ [_______________] │ │ [________] │ [________] │
│ │ │ │
│ Email │ │ City │ State │ Zip │
│ [_______________] │ │ [_____]│ [___] │ [____] │
└───────────────────────┘ └───────────────────────────┘
Completion rate: higher Completion rate: lower
Exception: Short, related fields can share a row: City / State / Zip, or First name / Last name, but only if they're logically grouped and the form remains scannable.
Label Placement
| Placement | Speed | Density | Best For |
|---|---|---|---|
| Above field | Fastest completion | Lower (more vertical space) | Most forms, mobile, accessibility |
| Left of field | Slower (eye zigzags) | Higher (less vertical space) | Data-heavy desktop forms where density matters |
| Floating label | Fast (once understood) | Moderate | Sign-in, simple forms (but has accessibility concerns) |
| Inside field (placeholder only) | Worst (label disappears) | Highest | Never use as the only label |
Recommendation: Labels above fields is the safest default. It works on all screen sizes, meets accessibility requirements, and has the fastest completion times in research studies.
Form Structure
┌─────────────────────────────────────────────┐
│ Section Title (e.g., "Personal Information")│
│ │
│ Label ← Visible label
│ ┌─────────────────────────────┐ ← Clear input boundary
│ │ Placeholder text │ ← Example, not label
│ └─────────────────────────────┘
│ Helper text appears here ← Optional guidance
│ │
│ Label * ← Required indicator
│ ┌─────────────────────────────┐
│ │ │
│ └─────────────────────────────┘
│ ✗ Error message in red ← Inline error
│ │
│ [Continue →] ← Clear action
└─────────────────────────────────────────────┘
Input Types
Always use the correct HTML5 input type. It determines the mobile keyboard, enables browser autofill, and provides free validation.
| Data | HTML | Mobile Keyboard | Additional Attributes |
|---|---|---|---|
type="email" | @ key visible | autocomplete="email" | |
| Phone | type="tel" | Numeric pad | autocomplete="tel" |
| Credit card | inputmode="numeric" | Numeric pad | autocomplete="cc-number" |
| Amount/price | inputmode="decimal" | Numeric with decimal | None |
| PIN/OTP | inputmode="numeric" | Numeric pad | autocomplete="one-time-code" |
| URL | type="url" | .com and / keys | autocomplete="url" |
| Search | type="search" | Search key + clear | None |
| Date | type="date" | Native date picker | None |
| Password | type="password" | Obscured | autocomplete="new-password" or autocomplete="current-password" |
Choosing the Right Control
| Question | Options ≤ 5 | Options 6-15 | Options 15+ |
|---|---|---|---|
| Select one | Radio buttons | Select dropdown | Select with search |
| Select multiple | Checkboxes | Checkbox list | Multi-select with search |
| Yes/No | Toggle or checkbox | N/A | N/A |
| Free text (short) | Text input | N/A | N/A |
| Free text (long) | Textarea | N/A | N/A |
Rule: If there are fewer than 5 options, show them all (radio/checkbox). Don't hide them behind a dropdown.
Validation
When to Validate
| Timing | How It Works | Best For |
|---|---|---|
| On blur (recommended) | Validates when user leaves the field | Most fields. Gives user time to finish typing |
| On submit | Validates all fields when form is submitted | Fallback when on-blur isn't implemented |
| On input (real-time) | Validates as user types, keystroke by keystroke | Password strength, character counters, search |
| On focus (re-validation) | Re-validate previously errored field when user returns | Previously invalid fields |
Validation Strategy
1. User fills in field → No validation yet (let them type)
2. User leaves field (blur) → Validate this field
3. If valid → Show subtle green check (optional)
4. If invalid → Show inline error message
5. User returns to fix → Re-validate on every change (on input)
6. When fixed → Immediately show success state
7. User submits → Validate all fields, scroll to first error
Never validate:
- While the user is still actively typing (except password strength)
- Before the user has interacted with the field at all
- By clearing the field content on error
Inline Validation Example
DURING TYPING (no validation feedback):
Email
┌─────────────────────────┐
│ jane@exam │
└─────────────────────────┘
AFTER BLUR (valid):
Email
┌─────────────────────────┐ ✓
│ jane@example.com │
└─────────────────────────┘
AFTER BLUR (invalid):
Email
┌─────────────────────────┐ ✗
│ jane@exam │ ← Red border
└─────────────────────────┘
Please enter a complete email address (e.g., name@example.com)
Error Messages
Anatomy of a Good Error Message
Every error message should contain:
- What went wrong: stated clearly
- How to fix it: actionable instruction
- An example: when the format isn't obvious
| Bad Error | Good Error |
|---|---|
| "Invalid input" | "Enter a valid email (e.g., name@example.com)" |
| "Error" | "Password must be at least 8 characters" |
| "Error code: VAL_001" | "This email is already registered. [Sign in instead?]" |
| "Field required" | "Enter your phone number to receive delivery updates" |
| "Invalid date" | "Enter a date in MM/DD/YYYY format" |
| "Password error" | "Passwords don't match. Re-enter your password" |
Error Display Guidelines
| Guideline | Why |
|---|---|
| Show error inline, directly below the field | User can see the error without scrolling |
| Use red border + red text + error icon | Redundant signals (not just color) for accessibility |
| Don't use alerts/modals for validation errors | Disruptive and forces dismissal before fixing |
| Scroll to and focus the first error on submit | User knows exactly where to start fixing |
| Don't clear the field on error | User loses their input and has to retype everything |
| Preserve error until fixed | Don't remove the error when user starts typing elsewhere |
Use aria-describedby to link error to field | Screen readers announce the error message |
<label for="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid="true"
class="input-error"
>
<span id="email-error" class="error-message" role="alert">
Please enter a valid email address (e.g., name@example.com)
</span>
Password Fields
Password Creation UX
Show requirements as the user types with real-time feedback:
Password
┌─────────────────────────────────────┐
│ ●●●●●●●●●● │ [👁 Show]
└─────────────────────────────────────┘
Password strength: ████████░░ Strong
Requirements:
✓ At least 8 characters
✓ One uppercase letter
✓ One lowercase letter
✗ One number
✗ One special character (!@#$%)
Password Best Practices
| Practice | Why |
|---|---|
| Show/hide toggle | Users need to verify what they typed, especially on mobile |
| Show strength meter | More motivating than a list of rules |
| Don't require periodic changes | NIST recommends against it; it causes weaker passwords |
| Allow paste into password fields | Password managers need this |
| Check against breached password lists | Have I Been Pwned API |
Use autocomplete="new-password" | Triggers password manager to suggest a strong password |
Smart Defaults and Auto-Complete
Reduce effort by pre-filling what you can:
| Technique | Implementation | Impact |
|---|---|---|
| Browser autofill | Use correct autocomplete attributes | Fills name, email, address, card in seconds |
| Geolocation defaults | Detect country/timezone from IP | Skip country selection for most users |
| Previous values | Remember last-used settings | Returning users complete forms faster |
| Smart detection | Detect card type from number, format phone as typed | Fewer errors, less thinking |
| Pre-selected defaults | Check the most common option by default | 80% of users won't need to change it |
Essential Autocomplete Values
<input autocomplete="given-name"> <!-- First name -->
<input autocomplete="family-name"> <!-- Last name -->
<input autocomplete="email"> <!-- Email -->
<input autocomplete="tel"> <!-- Phone -->
<input autocomplete="street-address"> <!-- Address line -->
<input autocomplete="address-level2"> <!-- City -->
<input autocomplete="address-level1"> <!-- State/Province -->
<input autocomplete="postal-code"> <!-- ZIP/Postal code -->
<input autocomplete="country"> <!-- Country -->
<input autocomplete="cc-number"> <!-- Credit card number -->
<input autocomplete="cc-exp"> <!-- Card expiry -->
<input autocomplete="cc-csc"> <!-- Card CVC -->
Progressive Disclosure
Only show fields when they're relevant. This reduces cognitive load and makes the form feel shorter.
BEFORE (all fields visible):
┌─────────────────────────────────────────┐
│ Shipping address │
│ [___________________________________] │
│ Billing address │
│ [___________________________________] │
│ Company name (if applicable) │
│ [___________________________________] │
│ VAT number (if applicable) │
│ [___________________________________] │
│ Gift message (if applicable) │
│ [___________________________________] │
└─────────────────────────────────────────┘
AFTER (progressive disclosure):
┌─────────────────────────────────────────┐
│ Shipping address │
│ [___________________________________] │
│ │
│ ☑ Billing address same as shipping │
│ │
│ ☐ This is a business purchase │
│ (shows Company + VAT fields if checked) │
│ │
│ ☐ Add a gift message │
│ (shows message field if checked) │
└─────────────────────────────────────────┘
Multi-Step Forms
When to Split a Form
- The form has more than 6-8 fields
- Fields fall into distinct logical groups
- Some fields depend on earlier answers
- The form has a high abandonment rate
Multi-Step Form Design
Step 1 of 4 Step 2 of 4 Step 3 of 4 Step 4 of 4
Account → Personal → Preferences → Review
●────────────────○────────────────○────────────────○
| Guideline | Why |
|---|---|
| Show total steps and current position | Sets expectations, reduces anxiety |
| Allow backward navigation | Users need to review and change earlier answers |
| Save progress automatically | Users may abandon and return later |
| Summarize on final step | Lets users verify before submitting |
| Validate each step before advancing | Don't let errors accumulate to the end |
| Keep steps roughly equal in length | One 12-field step + three 2-field steps feels unbalanced |
| Use descriptive step labels | "Personal Info" not "Step 1" |
Multi-Step Progress Indicators
STYLE 1: Connected dots with labels
● Account ─── ○ Personal ─── ○ Payment ─── ○ Review
(completed) (current) (upcoming) (upcoming)
STYLE 2: Numbered steps
✓ 1 ─── ● 2 ─── ○ 3 ─── ○ 4
STYLE 3: Progress bar
████████████████░░░░░░░░░░░░░░ Step 2 of 4
Required vs. Optional Fields
Two schools of thought:
| Approach | When to Use |
|---|---|
| Mark optional fields with "(optional)" | When most fields are required |
| Mark required fields with * and legend | When many fields are optional |
Preferred approach: Make every field required unless there's a good reason not to. Then mark the few optional ones with "(optional)" in the label. This reduces the number of markers needed and reduces form length overall (remove fields you don't need).
Full name
[_______________]
Email
[_______________]
Phone (optional)
[_______________]
Message
[_______________]
Accessible Form Patterns
| Requirement | Implementation |
|---|---|
| Every input has a visible label | <label for="field-id"> linked to input |
| Error messages linked to fields | aria-describedby="error-id" on the input |
| Required fields announced | aria-required="true" or HTML required |
| Invalid state announced | aria-invalid="true" when validation fails |
| Form errors announced | Error summary with role="alert" or aria-live="polite" |
| Fieldsets for groups | <fieldset> + <legend> for radio/checkbox groups |
| Visible focus indicators | 2px+ focus ring on all interactive elements |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Placeholder as only label | Label disappears when typing, fails accessibility | Always use a visible <label> above the field |
| Clearing fields on error | User loses all input, rage-inducing | Preserve input and highlight the error |
| Validating while typing | Error appears mid-word ("Invalid email" after typing "j") | Validate on blur, re-validate on input for errored fields |
| No error summary on submit | User can't find the errors | Scroll to first error and show an error count summary |
| Generic error messages | User can't fix the problem | Be specific: what's wrong + how to fix it + example |
| Too many fields | Abandonment increases with every field | Remove every field that isn't essential. Can you get it later? |
| Missing autocomplete attributes | User types everything manually | Add autocomplete to every personal data field |
| Multi-column layout | Breaks scanning, causes errors | Use single-column for most forms |
Key Takeaways
- Single-column, labels above fields, validate on blur. These three decisions alone fix most form problems.
- Every error message needs: what's wrong, how to fix it, and (ideally) an example.
- Use the right input type and autocomplete attributes. They give you free mobile keyboards and autofill.
- Show fewer than 5 options as radio/checkboxes, not dropdowns. Make choices visible.
- Use progressive disclosure to hide conditional fields until they're relevant.
- Split forms with more than 6-8 fields into multi-step forms with clear progress indicators.
- Remove every field that isn't absolutely necessary. Every field you remove increases completion rates.
- Test forms with real users completing real tasks. Form problems only surface during actual use.