· Accessibility

Building Accessible Forms: A Complete Guide

Create forms that work for everyone with this comprehensive accessibility guide.

Forms are one of the most important parts of any website—they’re how users sign up, log in, contact you, and complete purchases. But poorly designed forms create barriers that prevent millions of people from using your site. In this comprehensive guide, you’ll learn how to build forms that work for everyone, regardless of their abilities or the tools they use to access the web.

Accessible forms aren’t just good for users with disabilities—they’re better for everyone. Clear labels, logical structure, and helpful error messages make forms easier and faster to complete for all users.

Why Form Accessibility Matters

The Reality of Web Accessibility

According to the WHO, over 1 billion people worldwide live with some form of disability. That’s roughly 15% of your potential users who might struggle with inaccessible forms. This includes people who:

  • Use screen readers to navigate websites
  • Have motor disabilities and use keyboard-only navigation
  • Have visual impairments but don’t use screen readers
  • Have cognitive disabilities that make complex forms challenging
  • Use voice control software to interact with websites

The Business Case

Beyond the moral imperative, accessible forms make business sense:

  • Increased conversions - Clear, well-structured forms have higher completion rates
  • Legal compliance - Many countries require web accessibility (ADA in US, EAA in EU)
  • Better SEO - Accessible HTML is cleaner and more semantic, which search engines favor
  • Wider audience - Don’t exclude 15% of potential customers or users

The Foundation: Proper HTML Structure

Always Use Labels

Every form input must have a label. This is the single most important rule for form accessibility.

<!-- Problematic example: No label, just a placeholder -->
<input type="text" placeholder="Enter your email" />

<!-- Problematic example: Label not connected to input -->
<label>Email</label>
<input type="email" />

<!-- Recommended approach: Label connected with for/id -->
<label for="email">Email Address</label>
<input id="email" type="email" name="email" />

<!-- Acceptable alternative: label wrapping the input -->
<label>
  Email Address
  <input type="email" name="email" />
</label>

Why this matters: Screen readers announce the label when users focus on an input. Without a proper label, users have no idea what information to enter.

Use the Right Input Types

HTML5 input types provide built-in validation and show appropriate keyboards on mobile devices. They’re also better for screen readers.

<!-- Recommended practice: use specific input types -->
<label for="email">Email Address</label>
<input id="email" type="email" name="email" required />

<label for="phone">Phone Number</label>
<input id="phone" type="tel" name="phone" />

<label for="website">Website URL</label>
<input id="website" type="url" name="website" />

<label for="birthdate">Date of Birth</label>
<input id="birthdate" type="date" name="birthdate" />

<label for="quantity">Quantity</label>
<input id="quantity" type="number" min="1" max="10" name="quantity" />

Use <fieldset> and <legend> to group related form fields. This provides crucial context for screen reader users.

<!-- Recommended structure: group contact information -->
<form>
  <fieldset>
    <legend>Contact Information</legend>
    
    <label for="name">Full Name</label>
    <input id="name" type="text" name="name" required />
    
    <label for="email">Email</label>
    <input id="email" type="email" name="email" required />
    
    <label for="phone">Phone</label>
    <input id="phone" type="tel" name="phone" />
  </fieldset>

  <fieldset>
    <legend>Shipping Address</legend>
    
    <label for="street">Street Address</label>
    <input id="street" type="text" name="street" />
    
    <label for="city">City</label>
    <input id="city" type="text" name="city" />
    
    <label for="zip">ZIP Code</label>
    <input id="zip" type="text" name="zip" />
  </fieldset>

  <button type="submit">Submit Order</button>
</form>

Radio Buttons and Checkboxes Done Right

Radio buttons and checkboxes require special attention because they involve multiple related inputs.

Radio Button Groups

<!-- Recommended structure: properly organized radio buttons -->
<fieldset>
  <legend>Choose your shipping method</legend>
  
  <label>
    <input type="radio" name="shipping" value="standard" checked />
    Standard Shipping (5-7 days) - Free
  </label>
  
  <label>
    <input type="radio" name="shipping" value="express" />
    Express Shipping (2-3 days) - $9.99
  </label>
  
  <label>
    <input type="radio" name="shipping" value="overnight" />
    Overnight Shipping - $24.99
  </label>
</fieldset>

Key points:

  • Use <fieldset> and <legend> to group radio buttons
  • All radio buttons in a group must have the same name attribute
  • Labels should describe both the option and any important details

Checkbox Groups

<!-- Recommended structure: multiple checkboxes with clear labels -->
<fieldset>
  <legend>Select your interests</legend>
  
  <label>
    <input type="checkbox" name="interests" value="web-dev" />
    Web Development
  </label>
  
  <label>
    <input type="checkbox" name="interests" value="design" />
    Design
  </label>
  
  <label>
    <input type="checkbox" name="interests" value="marketing" />
    Marketing
  </label>
</fieldset>

<!-- Recommended practice: single checkbox for agreements -->
<label>
  <input type="checkbox" name="terms" required />
  I agree to the <a href="/terms">terms and conditions</a>
</label>

Required Fields and Validation

Marking Required Fields

Always make it clear which fields are required, both visually and programmatically.

<!-- Recommended practice: use the required attribute -->
<label for="name">
  Full Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" name="name" required />

<!-- Acceptable alternative: text indicator -->
<label for="email">
  Email Address <span>(required)</span>
</label>
<input id="email" type="email" name="email" required aria-required="true" />

Style tip: Don’t rely on color alone to indicate required fields. Use text, symbols, or both.

Error Messages That Help

When validation fails, provide clear, specific error messages that tell users exactly what’s wrong and how to fix it.

<!-- Problematic example: Generic error, no context -->
<input type="email" />
<span class="error">Invalid</span>

<!-- Recommended approach: Specific, helpful error -->
<label for="email">Email Address</label>
<input 
  id="email" 
  type="email" 
  name="email" 
  aria-describedby="email-error"
  aria-invalid="true"
/>
<span id="email-error" class="error" role="alert">
  Please enter a valid email address (e.g., [email protected])
</span>

Client-Side Validation with ARIA

Use ARIA attributes to connect inputs with error messages and hint text.

<!-- Example with helpful hints and errors -->
<label for="password">Password</label>
<input 
  id="password" 
  type="password" 
  name="password"
  aria-describedby="password-hint password-error"
  aria-invalid="true"
  required
/>
<div id="password-hint">
  Must be at least 8 characters with one number and one special character
</div>
<div id="password-error" class="error" role="alert">
  Password must contain at least one number
</div>

Select elements can be tricky for accessibility. Keep them simple and provide clear labels.

<!-- Recommended baseline: select element with a label -->
<label for="country">Country</label>
<select id="country" name="country" required>
  <option value="">-- Please select --</option>
  <option value="us">United States</option>
  <option value="ca">Canada</option>
  <option value="uk">United Kingdom</option>
  <option value="au">Australia</option>
</select>

<!-- Recommended structure: grouped options for better organization -->
<label for="timezone">Time Zone</label>
<select id="timezone" name="timezone">
  <option value="">-- Select timezone --</option>
  
  <optgroup label="United States">
    <option value="est">Eastern Time</option>
    <option value="cst">Central Time</option>
    <option value="mst">Mountain Time</option>
    <option value="pst">Pacific Time</option>
  </optgroup>
  
  <optgroup label="Europe">
    <option value="gmt">GMT</option>
    <option value="cet">Central European Time</option>
    <option value="eet">Eastern European Time</option>
  </optgroup>
</select>

Keyboard Navigation

All form elements must be fully usable with just a keyboard. Most HTML form controls have this built-in, but you need to ensure:

Tab Order

Elements should receive focus in a logical order—typically top to bottom, left to right.

<!-- Recommended example: logical tab order -->
<form>
  <!-- tabindex not needed - natural HTML order works -->
  <label for="first-name">First Name</label>
  <input id="first-name" type="text" />
  
  <label for="last-name">Last Name</label>
  <input id="last-name" type="text" />
  
  <label for="email">Email</label>
  <input id="email" type="email" />
  
  <button type="submit">Submit</button>
</form>

Avoid: Using positive tabindex values (like tabindex="1", tabindex="2") as they override natural tab order and are hard to maintain.

For long forms, consider adding skip links to help keyboard users jump to different sections.

<form>
  <nav aria-label="Form navigation">
    <a href="#contact-info">Skip to Contact Information</a>
    <a href="#shipping-info">Skip to Shipping Information</a>
    <a href="#payment-info">Skip to Payment Information</a>
  </nav>

  <fieldset id="contact-info">
    <legend>Contact Information</legend>
    <!-- Fields here -->
  </fieldset>

  <fieldset id="shipping-info">
    <legend>Shipping Information</legend>
    <!-- Fields here -->
  </fieldset>

  <fieldset id="payment-info">
    <legend>Payment Information</legend>
    <!-- Fields here -->
  </fieldset>
</form>

Help Text and Instructions

Provide helpful instructions where needed, but keep them concise and relevant.

<!-- Recommended approach: use aria-describedby for help text -->
<label for="username">Username</label>
<input 
  id="username" 
  type="text" 
  name="username"
  aria-describedby="username-help"
  required
/>
<div id="username-help">
  Choose a unique username between 3-20 characters. 
  Letters, numbers, and underscores only.
</div>

<!-- Recommended detail: help text for complex fields -->
<label for="card-number">Credit Card Number</label>
<input 
  id="card-number" 
  type="text" 
  name="card"
  autocomplete="cc-number"
  aria-describedby="card-help"
/>
<div id="card-help">
  Enter your 16-digit card number without spaces
</div>

Autocomplete for Better UX

Use the autocomplete attribute to help browsers autofill forms. This is especially helpful for users with cognitive disabilities.

<!-- Recommended approach: autocomplete for common fields -->
<form autocomplete="on">
  <label for="name">Full Name</label>
  <input id="name" type="text" name="name" autocomplete="name" />
  
  <label for="email">Email</label>
  <input id="email" type="email" name="email" autocomplete="email" />
  
  <label for="tel">Phone</label>
  <input id="tel" type="tel" name="tel" autocomplete="tel" />
  
  <label for="street">Street Address</label>
  <input id="street" type="text" name="street" autocomplete="street-address" />
  
  <label for="city">City</label>
  <input id="city" type="text" name="city" autocomplete="address-level2" />
  
  <label for="zip">ZIP Code</label>
  <input id="zip" type="text" name="zip" autocomplete="postal-code" />
  
  <label for="country">Country</label>
  <input id="country" type="text" name="country" autocomplete="country-name" />
</form>

Common autocomplete values:

  • name - Full name
  • given-name - First name
  • family-name - Last name
  • email - Email address
  • tel - Phone number
  • street-address - Full street address
  • postal-code - ZIP/postal code
  • country-name - Country name
  • cc-number - Credit card number
  • cc-exp - Credit card expiration date

Focus Indicators

Never remove focus indicators without providing a better alternative. Focus styles show keyboard users where they are in the form.

/* Problematic approach: removing focus outlines */
input:focus {
  outline: none;
}

/* Recommended approach: providing clear custom focus styles */
input:focus {
  outline: 3px solid #0066cc;
  outline-offset: 2px;
}

/* Preferred approach: use :focus-visible for keyboard-only focus */
input:focus {
  outline: none;
}

input:focus-visible {
  outline: 3px solid #0066cc;
  outline-offset: 2px;
}

Complete Example: Accessible Contact Form

Here’s a complete contact form that incorporates all these principles:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Contact Us - Accessible Form Example</title>
    <style>
      /* Basic styling - add your own styles */
      .form-group {
        margin-bottom: 1.5rem;
      }
      
      label {
        display: block;
        margin-bottom: 0.5rem;
        font-weight: bold;
      }
      
      input, textarea, select {
        width: 100%;
        padding: 0.5rem;
        font-size: 1rem;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      
      input:focus, textarea:focus, select:focus {
        outline: 3px solid #0066cc;
        outline-offset: 2px;
      }
      
      .required {
        color: #d00;
      }
      
      .help-text {
        font-size: 0.875rem;
        color: #666;
        margin-top: 0.25rem;
      }
      
      .error {
        color: #d00;
        font-size: 0.875rem;
        margin-top: 0.25rem;
      }
      
      button {
        background: #0066cc;
        color: white;
        padding: 0.75rem 2rem;
        font-size: 1rem;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      
      button:hover {
        background: #0052a3;
      }
      
      button:focus {
        outline: 3px solid #0066cc;
        outline-offset: 2px;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Contact Us</h1>
      <p>Have a question? Fill out the form below and we'll get back to you within 24 hours.</p>
      
      <form action="/submit-contact" method="post" novalidate>
        <fieldset>
          <legend>Your Information</legend>
          
          <div class="form-group">
            <label for="name">
              Full Name <span class="required" aria-label="required">*</span>
            </label>
            <input 
              id="name" 
              type="text" 
              name="name" 
              autocomplete="name"
              required 
              aria-required="true"
            />
          </div>
          
          <div class="form-group">
            <label for="email">
              Email Address <span class="required" aria-label="required">*</span>
            </label>
            <input 
              id="email" 
              type="email" 
              name="email" 
              autocomplete="email"
              aria-describedby="email-help"
              required 
              aria-required="true"
            />
            <div id="email-help" class="help-text">
              We'll never share your email with anyone else.
            </div>
          </div>
          
          <div class="form-group">
            <label for="phone">Phone Number (optional)</label>
            <input 
              id="phone" 
              type="tel" 
              name="phone" 
              autocomplete="tel"
              aria-describedby="phone-help"
            />
            <div id="phone-help" class="help-text">
              Include your area code (e.g., 555-123-4567)
            </div>
          </div>
        </fieldset>
        
        <fieldset>
          <legend>Your Message</legend>
          
          <div class="form-group">
            <label for="subject">
              Subject <span class="required" aria-label="required">*</span>
            </label>
            <select id="subject" name="subject" required aria-required="true">
              <option value="">-- Please select a topic --</option>
              <option value="general">General Inquiry</option>
              <option value="support">Technical Support</option>
              <option value="billing">Billing Question</option>
              <option value="feedback">Feedback</option>
            </select>
          </div>
          
          <div class="form-group">
            <label for="message">
              Message <span class="required" aria-label="required">*</span>
            </label>
            <textarea 
              id="message" 
              name="message" 
              rows="6"
              aria-describedby="message-help"
              required 
              aria-required="true"
            ></textarea>
            <div id="message-help" class="help-text">
              Please provide as much detail as possible (minimum 10 characters)
            </div>
          </div>
        </fieldset>
        
        <div class="form-group">
          <label>
            <input type="checkbox" name="newsletter" value="yes" />
            Send me occasional updates about new features
          </label>
        </div>
        
        <button type="submit">Send Message</button>
      </form>
    </main>
  </body>
</html>

Testing Your Forms

Manual Testing Checklist

Before launching your form, test it with:

  • Keyboard only - Can you complete the entire form using only Tab, Shift+Tab, Enter, and arrow keys?
  • Screen reader - Use NVDA (Windows), JAWS (Windows), or VoiceOver (Mac) to navigate the form
  • Zoom to 200% - Does the form still work when zoomed in?
  • Mobile devices - Test on actual phones and tablets, not just browser emulation
  • Different browsers - Test in Chrome, Firefox, Safari, and Edge

Automated Testing Tools

Use these tools to catch common accessibility issues:

  • axe DevTools (browser extension) - Free accessibility checker
  • WAVE (web tool) - WebAIM’s accessibility evaluation tool
  • Lighthouse (built into Chrome) - Includes accessibility audit
  • Pa11y (command-line) - Automated accessibility testing

Common Issues to Watch For

  • Missing labels on inputs
  • Low contrast between text and background
  • Inputs that can’t be reached with keyboard
  • Error messages not connected to inputs
  • Required fields not clearly marked
  • Forms that lose data when validation fails

Before You Launch: Checklist

Make sure your form has:

  • Every input has a label (using for and id or wrapping)
  • Appropriate input types (email, tel, url, etc.)
  • Related fields grouped with <fieldset> and <legend>
  • Required fields marked both visually and with required attribute
  • Error messages that are clear, specific, and connected with aria-describedby
  • Help text for complex or unusual inputs
  • Autocomplete attributes for common fields
  • Visible focus indicators on all interactive elements
  • Logical tab order (test with keyboard only)
  • Works with screen readers (test with at least one screen reader)

Keep Learning

Forms are complex, but these principles will get you started on the right path:

Want to practice building accessible forms? Try it in the htmlEditor.net playground—experiment with these techniques and see how they work in real-time!

Remember: An accessible form is a better form for everyone. The extra effort you put into accessibility pays off in higher conversion rates, happier users, and a more inclusive web.

← Back to all blog posts

    Share: