Form Design Playbook
Forms are the conversion gatekeepers of the web. A well-designed form respects users’ time, reduces cognitive load, and builds trust. A poorly designed form causes frustration, abandonment, and support tickets.
This playbook provides both human-readable guidance and machine-parseable specifications for AI agents implementing form functionality.
Quick Reference: Form Design Specifications
Section titled “Quick Reference: Form Design Specifications”Field Sizing Requirements
Section titled “Field Sizing Requirements”| Element | Minimum Size | Recommended | WCAG Reference |
|---|---|---|---|
| Touch target (buttons, inputs) | 24×24 CSS px | 44×44 CSS px | 2.5.5, 2.5.8 |
| Input height | 36px | 44-48px | — |
| Font size (labels, inputs) | 16px | 16-18px | 1.4.4 |
| Tap spacing between targets | 8px | 12px+ | 2.5.8 |
Contrast Requirements
Section titled “Contrast Requirements”| Element | Minimum Ratio | WCAG Reference |
|---|---|---|
| Label text | 4.5:1 | 1.4.3 |
| Input text | 4.5:1 | 1.4.3 |
| Placeholder text | 4.5:1 | 1.4.3 |
| Input borders | 3:1 | 1.4.11 |
| Error text | 4.5:1 | 1.4.3 |
| Focus indicator | 3:1 | 2.4.7 |
Autocomplete Tokens (Common)
Section titled “Autocomplete Tokens (Common)”| Field Type | Autocomplete Value |
|---|---|
| Full name | name |
| First name | given-name |
| Last name | family-name |
email | |
| Phone | tel |
| Street address | street-address |
| City | address-level2 |
| State/Province | address-level1 |
| Postal code | postal-code |
| Country | country-name |
| Credit card number | cc-number |
| Card expiration | cc-exp |
| Card CVV | cc-csc |
| Username | username |
| Current password | current-password |
| New password | new-password |
| One-time code | one-time-code |
Validation Rules (for MCP/AI)
Section titled “Validation Rules (for MCP/AI)”rules: # Label Requirements - id: form-label-visible severity: error check: "Every input has a visible, persistent label (not placeholder-only)" selector: "input, select, textarea" wcag: "1.3.1, 3.3.2 AA"
- id: form-label-associated severity: error check: "Labels are programmatically associated via for/id or wrapping" selector: "label, input, select, textarea" wcag: "1.3.1 AA"
- id: form-label-position severity: warning check: "Labels positioned above or to the left of inputs (not inside)" selector: "label" best_practice: true
# Input Configuration - id: form-input-type severity: warning check: "Input uses appropriate type (email, tel, number, url, date)" selector: "input" wcag: "1.3.5 AA"
- id: form-autocomplete severity: error check: "Inputs for personal data have autocomplete attribute" selector: "input[name*='name'], input[name*='email'], input[name*='phone'], input[name*='address']" wcag: "1.3.5 AA"
- id: form-inputmode severity: warning check: "Numeric inputs use inputmode='numeric' or 'decimal'" selector: "input[type='text']:has-numeric-content" best_practice: true
# Error Handling - id: form-error-association severity: error check: "Error messages linked to inputs via aria-describedby" selector: ".error-message, [role='alert']" wcag: "3.3.1 AA"
- id: form-error-visible severity: error check: "Errors use text, not color alone" selector: "[aria-invalid='true']" wcag: "1.4.1 AA"
- id: form-error-focus severity: error check: "First error field receives focus on submit" selector: "form" wcag: "3.3.1 AA"
# Required Fields - id: form-required-indicated severity: error check: "Required fields marked visually AND with aria-required or required" selector: "input[required], input[aria-required='true']" wcag: "3.3.2 AA"
# Touch Targets - id: form-target-size severity: error check: "Submit buttons ≥24×24 CSS px (44×44 preferred)" selector: "button[type='submit'], input[type='submit']" wcag: "2.5.5, 2.5.8 AA"
# Focus Management - id: form-focus-visible severity: error check: "All form controls have visible focus indicator" selector: "input:focus, select:focus, textarea:focus, button:focus" wcag: "2.4.7 AA"
- id: form-focus-order severity: error check: "Tab order follows visual order" selector: "form" wcag: "2.4.3 AA"Phase 1: Scope the Form
Section titled “Phase 1: Scope the Form”Define the Job to Be Done
Section titled “Define the Job to Be Done”Before designing fields, answer these questions:
WHAT is the user trying to accomplish?WHY do we need each piece of information?WHEN do we need it? (Now vs. later)WHAT is the minimum viable form?Remove Non-Essential Fields
Section titled “Remove Non-Essential Fields”Every field you add has a cost:
| Fields | Completion Rate Impact |
|---|---|
| 3 fields | Baseline |
| 4 fields | -3% to -5% |
| 5-6 fields | -10% to -15% |
| 7+ fields | -20% or more |
Research insight: Expedia increased profits by $12 million by removing one redundant field from their booking form.
Decision Logic: Keep or Remove Field
Section titled “Decision Logic: Keep or Remove Field”FOR each field: IF field is required by law/regulation: KEEP (mark as required) ELSE IF field is essential for the transaction: KEEP ELSE IF field can be derived from other data: REMOVE (auto-populate later) ELSE IF field can be asked later: DEFER to post-completion ELSE IF field is "nice to have": REMOVE
RESULT: Minimum viable formMap Privacy and Legal Requirements
Section titled “Map Privacy and Legal Requirements”| Data Type | Considerations |
|---|---|
| PII (name, email) | Privacy policy link, GDPR basis |
| Financial (card, bank) | PCI compliance, security indicators |
| Health | HIPAA compliance (US), special category (GDPR) |
| Location | Clear purpose explanation |
Phase 2: Structure and Flow
Section titled “Phase 2: Structure and Flow”Use Single-Column Layout
Section titled “Use Single-Column Layout”Baymard Institute research shows:
- 16% of sites use multi-column forms that cause abandonment
- Users complete single-column forms 15.4 seconds faster
- Multi-column layouts cause field-skipping errors
Exception: Related fields can be inline:
- First name + Last name
- City + State + ZIP
- Card number + Expiration + CVV
Group Related Fields
Section titled “Group Related Fields”<fieldset> <legend>Contact Information</legend> <!-- Name, email, phone fields --></fieldset>
<fieldset> <legend>Shipping Address</legend> <!-- Address fields --></fieldset>Order by Mental Model
Section titled “Order by Mental Model”- Identity: Who is this? (name, email)
- Details: What do they need? (product, quantity, options)
- Fulfillment: How to deliver? (address, shipping)
- Payment: How to pay? (card, billing)
- Confirmation: Is this correct? (review, submit)
Multi-Step Forms for Long Processes
Section titled “Multi-Step Forms for Long Processes”When to use multi-step:
- More than 8-10 fields
- Multiple distinct categories
- Legal/compliance requires separation
- Complex conditional logic
Multi-step requirements:
- Clear progress indicator with step labels
- Back button on every step (except first)
- Persist data between steps
- Allow review before final submit
<nav aria-label="Checkout progress"> <ol> <li aria-current="step">1. Contact</li> <li>2. Shipping</li> <li>3. Payment</li> <li>4. Review</li> </ol></nav>Phase 3: Field Design
Section titled “Phase 3: Field Design”Always Use Visible Labels
Section titled “Always Use Visible Labels”<!-- Correct: Visible label --><label for="email">Email address</label><input type="email" id="email" name="email" autocomplete="email">
<!-- Wrong: Placeholder as label --><input type="email" placeholder="Email address">Why placeholders fail:
- Disappear on focus (memory load)
- Often low contrast (accessibility)
- No persistent reference for validation
- Confuses password managers
Label Position
Section titled “Label Position”| Position | Use Case | Notes |
|---|---|---|
| Above input | Default, best for scanning | Works for all field types |
| Left of input | Compact horizontal forms | Align labels right for clean edge |
| Floating | Limited space | Ensure contrast when floated |
Provide Descriptions for Complex Fields
Section titled “Provide Descriptions for Complex Fields”<label for="password">Password</label><input type="password" id="password" aria-describedby="password-hint"><p id="password-hint" class="hint"> At least 8 characters with one number and one symbol</p>Set Correct Input Types
Section titled “Set Correct Input Types”| Data | Type | Inputmode | Keyboard Effect |
|---|---|---|---|
email | — | @ key, domain suggestions | |
| Phone | tel | — | Phone dial pad |
| Number (integer) | text | numeric | Number pad, no spinner |
| Number (decimal) | text | decimal | Number pad with decimal |
| URL | url | — | / key, .com suggestions |
| Search | search | — | Search/enter key |
| Date | date | — | Native date picker |
| Credit card | text | numeric | Number pad |
Use Appropriate Controls
Section titled “Use Appropriate Controls”| Options Count | Recommended Control |
|---|---|
| 2 options | Radio buttons or toggle |
| 3-5 options | Radio buttons |
| 6-10 options | Dropdown/select |
| 10+ options | Autocomplete/combobox |
| Yes/No | Checkbox or toggle |
| Multiple selections | Checkboxes or multi-select |
Phase 4: Defaults and Helpers
Section titled “Phase 4: Defaults and Helpers”Provide Safe Defaults
Section titled “Provide Safe Defaults”<!-- Default country based on locale --><select name="country" autocomplete="country-name"> <option value="US" selected>United States</option> <!-- Other options --></select>
<!-- Pre-checked for common preference --><input type="checkbox" id="email-optin" checked><label for="email-optin">Send me order updates via email</label>Format Hints and Examples
Section titled “Format Hints and Examples”<label for="phone">Phone number</label><input type="tel" id="phone" placeholder="(555) 123-4567" aria-describedby="phone-format"><p id="phone-format" class="hint"> Format: (555) 123-4567</p>Live Formatting Preview
Section titled “Live Formatting Preview”<!-- Credit card with live formatting --><label for="card">Card number</label><input type="text" id="card" inputmode="numeric" placeholder="1234 5678 9012 3456" autocomplete="cc-number">
<!-- Show card type icon based on input --><span class="card-type-icon" aria-label="Visa detected"></span>Important: Don’t block typing with auto-formatting. Let users type freely, format on blur or display separately.
Phase 5: Validation
Section titled “Phase 5: Validation”Validation Timing
Section titled “Validation Timing”| Timing | Use Case | User Experience |
|---|---|---|
| On blur | Most fields | Immediate feedback after leaving |
| On input (debounced) | Availability checks | Real-time username/email check |
| On submit | Final validation | Catch any missed errors |
| Never on focus | — | Don’t show errors before user tries |
Decision Logic: When to Validate
Section titled “Decision Logic: When to Validate”ON field blur: IF field is required AND empty: SHOW "This field is required" ELSE IF field has format constraint: VALIDATE format IF invalid: SHOW specific error ELSE IF field needs server check (email exists, username taken): DEBOUNCE 300ms, then check SHOW result
ON form submit: VALIDATE all fields IF any errors: MOVE focus to first error SHOW error summary at top (anchored links) PREVENT submission ELSE: SUBMIT formError Message Anatomy
Section titled “Error Message Anatomy”<!-- Field with error --><div class="field field--error"> <label for="email">Email address</label> <input type="email" id="email" aria-invalid="true" aria-describedby="email-error"> <p id="email-error" class="error" role="alert"> Please enter a valid email address (e.g., name@example.com) </p></div>Error Message Guidelines
Section titled “Error Message Guidelines”| Bad | Good | Why |
|---|---|---|
| ”Invalid input" | "Please enter a valid email address” | Specific, actionable |
| ”Error" | "Email is required” | Identifies the problem |
| ”Format error" | "Phone must be 10 digits” | Explains the fix |
| ”Required" | "Please enter your name” | Conversational, clear |
Error Summary
Section titled “Error Summary”<div role="alert" aria-labelledby="error-summary-title"> <h2 id="error-summary-title">Please fix 2 errors</h2> <ul> <li><a href="#email">Email address is invalid</a></li> <li><a href="#phone">Phone number is required</a></li> </ul></div>Don’t Block Input with Formatting
Section titled “Don’t Block Input with Formatting”WRONG approach: User types: 5551234567 System blocks: "Invalid format" User confused about what's wrong
RIGHT approach: User types: 5551234567 On blur, normalize: (555) 123-4567 OR display formatted preview separatelyPhase 6: Accessibility
Section titled “Phase 6: Accessibility”Label Association Patterns
Section titled “Label Association Patterns”<!-- Pattern 1: Explicit label (preferred) --><label for="name">Full name</label><input type="text" id="name" autocomplete="name">
<!-- Pattern 2: Wrapped label --><label> Full name <input type="text" autocomplete="name"></label>
<!-- Pattern 3: aria-labelledby for complex layouts --><span id="name-label">Full name</span><input type="text" aria-labelledby="name-label" autocomplete="name">Help and Error Text Association
Section titled “Help and Error Text Association”<label for="password">Password</label><input type="password" id="password" aria-describedby="password-hint password-error" aria-invalid="true"><p id="password-hint" class="hint"> Minimum 8 characters</p><p id="password-error" class="error" role="alert"> Password must contain at least one number</p>Focus Order and Visibility
Section titled “Focus Order and Visibility”/* Ensure visible focus */input:focus,select:focus,textarea:focus,button:focus { outline: 2px solid var(--focus-color); outline-offset: 2px;}
/* Never remove focus entirely *//* Wrong: *//* :focus { outline: none; } */Required Field Indication
Section titled “Required Field Indication”<!-- Visual AND programmatic indication --><label for="email"> Email address <span class="required" aria-hidden="true">*</span></label><input type="email" id="email" required aria-required="true">
<!-- Legend for form explaining asterisk --><p class="form-legend"> <span aria-hidden="true">*</span> Required field</p>Accessible Form Patterns
Section titled “Accessible Form Patterns”<form aria-labelledby="form-title" novalidate> <h1 id="form-title">Create an account</h1>
<!-- Error summary location --> <div id="error-summary" role="alert" aria-live="polite"></div>
<fieldset> <legend>Personal Information</legend> <!-- Fields --> </fieldset>
<button type="submit">Create account</button></form>Phase 7: Security and Trust
Section titled “Phase 7: Security and Trust”Indicate Why Sensitive Data Is Needed
Section titled “Indicate Why Sensitive Data Is Needed”<label for="ssn"> Social Security Number <button type="button" aria-label="Why we need this" data-tooltip="Required for tax reporting per IRS regulations"> <span aria-hidden="true">?</span> </button></label><input type="text" id="ssn" inputmode="numeric" autocomplete="off">Password Fields
Section titled “Password Fields”<label for="password">Password</label><div class="password-field"> <input type="password" id="password" autocomplete="new-password" aria-describedby="password-requirements"> <button type="button" aria-label="Show password" onclick="togglePasswordVisibility()"> <span class="icon-eye"></span> </button></div><ul id="password-requirements" class="hint"> <li>At least 8 characters</li> <li>One uppercase letter</li> <li>One number</li></ul>Trust Indicators
Section titled “Trust Indicators”| Indicator | Placement | Impact |
|---|---|---|
| Security badges | Near payment fields | +29% perceived trust |
| Privacy policy link | Near submit | Reduces hesitation |
| Secure connection (HTTPS) | Browser shows this | Baseline expectation |
| ”We’ll never share” | Near email field | Reduces friction |
Autosave and Data Loss Prevention
Section titled “Autosave and Data Loss Prevention”// Autosave form data to localStorageform.addEventListener('input', debounce(() => { const formData = new FormData(form); localStorage.setItem('draft-form', JSON.stringify(Object.fromEntries(formData)));}, 1000));
// Warn before leavingwindow.addEventListener('beforeunload', (e) => { if (formHasUnsavedChanges) { e.preventDefault(); e.returnValue = ''; }});Phase 8: Review and Confirmation
Section titled “Phase 8: Review and Confirmation”Review Step for Important Transactions
Section titled “Review Step for Important Transactions”<section aria-labelledby="review-title"> <h2 id="review-title">Review your order</h2>
<dl class="review-summary"> <dt>Shipping address</dt> <dd> 123 Main St, Anytown, ST 12345 <a href="#step-shipping">Edit</a> </dd>
<dt>Payment method</dt> <dd> Visa ending in 4242 <a href="#step-payment">Edit</a> </dd> </dl>
<button type="submit">Place order</button></section>Confirmation Patterns
Section titled “Confirmation Patterns”| Action | Confirmation Type |
|---|---|
| Order placed | Confirmation page + email |
| Account created | Success message + email verification |
| Form submitted | Thank you page with next steps |
| Destructive action | Undo option (time-limited) |
Confirmation Page Content
Section titled “Confirmation Page Content”- Clear success message
- Reference number/order ID
- Summary of submitted data
- Expected next steps and timeline
- Contact information for support
- Print/save option
Phase 9: Implementation Checklist
Section titled “Phase 9: Implementation Checklist”HTML Structure
Section titled “HTML Structure”- Form has accessible name (
aria-labelledbyor<legend>) - Labels associated with inputs (for/id or wrapping)
- Required fields marked visually AND with
required/aria-required - Hints and errors linked via
aria-describedby - Fieldsets group related fields with legends
- Form uses
novalidatefor custom validation - Submit button has clear, action-oriented text
Input Configuration
Section titled “Input Configuration”- Correct
typeattribute for each input -
autocompletetokens for all personal data fields -
inputmodefor numeric fields without spinners -
patternattribute where format validation needed -
maxlengthfor character-limited fields
Validation
Section titled “Validation”- Client-side validation on blur and submit
- Server-side validation (never trust client)
- Error messages are specific and actionable
- First error receives focus on submit
- Error summary at top with anchor links
-
aria-invalid="true"on fields with errors - Error messages have
role="alert"
Accessibility
Section titled “Accessibility”- Keyboard navigation works (Tab, Shift+Tab, Enter)
- Focus order matches visual order
- Focus indicator visible (3:1 contrast)
- Screen reader testing completed (NVDA, VoiceOver)
- Label contrast ≥4.5:1
- Input border contrast ≥3:1
- Touch targets ≥44×44px (minimum 24×24)
Security
Section titled “Security”- HTTPS on form page
- CSRF token included
- Sensitive data explanation where needed
- Password show/hide toggle
- Autosave for long forms
- Data loss warning on navigation
Performance
Section titled “Performance”- Form loads without JavaScript (progressive enhancement)
- Validation doesn’t block typing
- Debounced server-side checks
- Optimistic UI where appropriate
- Idempotent submission handling
Metrics and Iteration
Section titled “Metrics and Iteration”Key Metrics to Track
Section titled “Key Metrics to Track”| Metric | What It Tells You |
|---|---|
| Step drop-off rate | Which steps lose users |
| Field-level abandon | Which fields cause friction |
| Time-to-complete | Overall complexity |
| Error rate by field | Confusing fields |
| Validation error rate | Format/requirement issues |
| Submission success rate | End-to-end conversion |
A/B Testing Ideas
Section titled “A/B Testing Ideas”| Test | Hypothesis |
|---|---|
| Remove optional fields | Higher completion |
| Add progress indicator | Lower abandon on multi-step |
| Inline vs. top error summary | Faster error recovery |
| Single vs. multi-column | Fewer errors |
| Floating vs. above labels | Comparable completion |
| Required asterisk vs. optional label | Lower error rate |
Common Issues from Support Tickets
Section titled “Common Issues from Support Tickets”| Issue | Likely Cause | Fix |
|---|---|---|
| ”Can’t submit form” | Validation blocking invisibly | Show all errors clearly |
| ”Lost my progress” | No autosave | Implement draft saving |
| ”Format confusion” | Unclear requirements | Add format hints |
| ”Mobile keyboard wrong” | Missing inputmode/type | Add correct attributes |
| ”Can’t find X field” | Poor grouping | Reorganize with fieldsets |
Code Patterns
Section titled “Code Patterns”Complete Accessible Form Field
Section titled “Complete Accessible Form Field”<div class="form-field" data-field="email"> <label for="email" class="form-label"> Email address <span class="required-indicator" aria-hidden="true">*</span> </label> <input type="email" id="email" name="email" class="form-input" autocomplete="email" required aria-required="true" aria-describedby="email-hint email-error" aria-invalid="false" > <p id="email-hint" class="form-hint"> We'll send your order confirmation here </p> <p id="email-error" class="form-error" role="alert" hidden> <!-- Error message inserted by JavaScript --> </p></div>Form Validation JavaScript
Section titled “Form Validation JavaScript”class FormValidator { constructor(form) { this.form = form; this.fields = form.querySelectorAll('input, select, textarea');
this.fields.forEach(field => { field.addEventListener('blur', () => this.validateField(field)); });
form.addEventListener('submit', (e) => this.handleSubmit(e)); }
validateField(field) { const error = this.getFieldError(field); this.showFieldError(field, error); return !error; }
getFieldError(field) { if (field.required && !field.value.trim()) { return `Please enter your ${this.getFieldLabel(field)}`; } if (field.type === 'email' && !this.isValidEmail(field.value)) { return 'Please enter a valid email address'; } // Add more validation rules return null; }
showFieldError(field, error) { const errorElement = document.getElementById(`${field.id}-error`); field.setAttribute('aria-invalid', error ? 'true' : 'false');
if (error) { errorElement.textContent = error; errorElement.hidden = false; } else { errorElement.hidden = true; } }
handleSubmit(e) { const errors = [];
this.fields.forEach(field => { if (!this.validateField(field)) { errors.push(field); } });
if (errors.length > 0) { e.preventDefault(); errors[0].focus(); this.showErrorSummary(errors); } }}CSS Form Styles
Section titled “CSS Form Styles”.form-field { margin-bottom: var(--space-6);}
.form-label { display: block; margin-bottom: var(--space-2); font-weight: var(--font-weight-medium); color: var(--color-text-default);}
.required-indicator { color: var(--color-error); margin-left: var(--space-1);}
.form-input { display: block; width: 100%; padding: var(--space-3) var(--space-4); font-size: var(--font-size-base); line-height: 1.5; color: var(--color-text-default); background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--border-radius-md); min-height: 44px;}
.form-input:focus { outline: 2px solid var(--color-interactive-default); outline-offset: 2px; border-color: var(--color-interactive-default);}
.form-input[aria-invalid="true"] { border-color: var(--color-error);}
.form-hint { margin-top: var(--space-2); font-size: var(--font-size-sm); color: var(--color-text-muted);}
.form-error { margin-top: var(--space-2); font-size: var(--font-size-sm); color: var(--color-error);}
.form-error::before { content: "⚠ ";}Recent Research (2024-2025)
Section titled “Recent Research (2024-2025)”Form Abandonment Statistics
Section titled “Form Abandonment Statistics”Baymard Institute research shows:
- 18% of users abandon carts due to forms perceived as too long/complex
- 16% of sites use multi-column layouts that cause errors
- Average 35% increase in conversion with checkout UX improvements
Legal Landscape Changes
Section titled “Legal Landscape Changes”- US DOJ updated ADA Title II in April 2024 requiring WCAG 2.1 AA compliance
- European Accessibility Act fully enforceable in 2025
- Accessibility lawsuits increased 14% in 2024
Field Count Impact
Section titled “Field Count Impact”WPForms research found:
- 30%+ of marketers report highest conversions with 4-field forms
- CAPTCHAs can reduce conversions by up to 40%
- Adding social proof increases conversion up to 26%
Inline Validation Preference
Section titled “Inline Validation Preference”Reform.app research confirms inline validation is preferred over submit-only validation, with real-time feedback reducing user frustration and abandonment.
References
Section titled “References”Foundational Work:
- Web Form Design (Book) — Luke Wroblewski — The definitive guide to form design
- LukeW: Form Design Articles — Ongoing research and insights on form patterns
Research:
- Baymard Institute: Form Design Best Practices
- Baymard: Avoid Multi-Column Forms
- WPForms: Online Form Statistics
Standards:
Practical Guides:
- UXPin: Accessible Form Validation
- UXPin: Ultimate Guide to Accessible Forms
- Accessibility Checker: Accessible Forms
See Also
Section titled “See Also”- ARIA & Keyboard Patterns — Focus management and live regions
- Touch Targets & Spacing — Button and input sizing
- Accessible Typography — Label and error text sizing
- Error Types — Human error patterns
- Defensive Design — Preventing user mistakes
- Content & Microcopy Templates — Error message writing