Skip to main content
Technology & EngineeringAccessibility229 lines

Accessible Forms

Accessible form design patterns including labels, validation, error handling, and multi-step forms

Quick Summary27 lines
You are an expert in accessible form patterns for building accessible web applications.

## Key Points

1. A `<label>` element with a matching `for`/`id` pair (preferred).
2. A wrapping `<label>` element.
3. `aria-label` (when a visible label is not feasible).
4. `aria-labelledby` pointing to another element's ID.
- Identify the field in error by name.
- Describe what went wrong in plain language.
- Suggest how to fix it.
- Associate the error message with the input using `aria-describedby`.
- Move focus to the first error or to an error summary.
- Click on every `<label>` and verify it focuses the associated input.
- Submit the form with empty required fields and confirm that error messages are announced by screen readers and that focus moves to the error summary or the first invalid field.
- Verify every input has an accessible name in the browser's accessibility tree.

## Quick Example

```html
<div>
  <label for="full-name">Full name</label>
  <input type="text" id="full-name" name="fullName" autocomplete="name" required>
</div>
```
skilldb get accessibility-skills/Accessible FormsFull skill: 229 lines
Paste into your CLAUDE.md or agent config

Accessible Forms — Web Accessibility

You are an expert in accessible form patterns for building accessible web applications.

Core Philosophy

Overview

Forms are the primary way users interact with web applications—logging in, searching, submitting data, and configuring settings. Inaccessible forms are one of the most common barriers on the web. Accessible forms require proper labeling, clear instructions, descriptive error messages, logical grouping, and keyboard operability.

Core Concepts

Every input needs an accessible name

An input's accessible name can come from:

  1. A <label> element with a matching for/id pair (preferred).
  2. A wrapping <label> element.
  3. aria-label (when a visible label is not feasible).
  4. aria-labelledby pointing to another element's ID.

Placeholder text is not an accessible name—it disappears on input and often has insufficient contrast.

Grouping related controls

Use <fieldset> and <legend> to group related inputs (radio buttons, checkboxes, address fields). Screen readers announce the legend text before each control in the group, providing context.

Error handling principles

  • Identify the field in error by name.
  • Describe what went wrong in plain language.
  • Suggest how to fix it.
  • Associate the error message with the input using aria-describedby.
  • Move focus to the first error or to an error summary.

Implementation Patterns

Basic labeled input

<div>
  <label for="full-name">Full name</label>
  <input type="text" id="full-name" name="fullName" autocomplete="name" required>
</div>

Required field indication

<p id="required-hint">Fields marked with <span aria-hidden="true">*</span>
  <span class="sr-only">asterisk</span> are required.</p>

<div>
  <label for="email">
    Email address <span aria-hidden="true">*</span>
  </label>
  <input
    type="email"
    id="email"
    name="email"
    required
    aria-required="true"
    aria-describedby="required-hint"
    autocomplete="email"
  >
</div>

Fieldset for related controls

<fieldset>
  <legend>Preferred contact method</legend>
  <div>
    <input type="radio" id="contact-email" name="contact" value="email">
    <label for="contact-email">Email</label>
  </div>
  <div>
    <input type="radio" id="contact-phone" name="contact" value="phone">
    <label for="contact-phone">Phone</label>
  </div>
  <div>
    <input type="radio" id="contact-sms" name="contact" value="sms">
    <label for="contact-sms">Text message</label>
  </div>
</fieldset>

Inline error messages

<div>
  <label for="password">Password</label>
  <input
    type="password"
    id="password"
    name="password"
    required
    aria-required="true"
    aria-invalid="true"
    aria-describedby="password-error password-hint"
    autocomplete="new-password"
  >
  <p id="password-hint" class="hint">Must be at least 8 characters.</p>
  <p id="password-error" class="error" role="alert">
    Error: Password is too short. Enter at least 8 characters.
  </p>
</div>

Error summary at the top of the form

<div role="alert" id="error-summary" tabindex="-1">
  <h2>There are 2 errors in this form</h2>
  <ul>
    <li><a href="#email">Email address is required</a></li>
    <li><a href="#password">Password must be at least 8 characters</a></li>
  </ul>
</div>
function handleSubmit(event) {
  event.preventDefault();
  const errors = validateForm();

  if (errors.length > 0) {
    renderErrorSummary(errors);
    // Focus the error summary so screen readers announce it
    document.getElementById('error-summary').focus();
  } else {
    submitForm();
  }
}

Accessible select / custom dropdown

<!-- Native select: always the most accessible option -->
<label for="country">Country</label>
<select id="country" name="country" autocomplete="country-name">
  <option value="">Select a country</option>
  <option value="us">United States</option>
  <option value="ca">Canada</option>
  <option value="uk">United Kingdom</option>
</select>

Multi-step form (wizard)

<nav aria-label="Form progress">
  <ol>
    <li aria-current="step">Step 1: Personal info</li>
    <li>Step 2: Preferences</li>
    <li>Step 3: Review</li>
  </ol>
</nav>

<form>
  <fieldset>
    <legend>Step 1 of 3: Personal information</legend>

    <div>
      <label for="first-name">First name</label>
      <input type="text" id="first-name" autocomplete="given-name" required>
    </div>
    <div>
      <label for="last-name">Last name</label>
      <input type="text" id="last-name" autocomplete="family-name" required>
    </div>

    <button type="button" onclick="goToStep(2)">Next: Preferences</button>
  </fieldset>
</form>

Autocomplete attributes

<!-- Use autocomplete to help users and password managers -->
<input type="text" autocomplete="given-name" name="firstName">
<input type="text" autocomplete="family-name" name="lastName">
<input type="email" autocomplete="email" name="email">
<input type="tel" autocomplete="tel" name="phone">
<input type="text" autocomplete="street-address" name="address">
<input type="text" autocomplete="postal-code" name="zip">
<input type="password" autocomplete="new-password" name="password">
<input type="text" autocomplete="one-time-code" name="otp" inputmode="numeric">

Testing & Validation

  • Click on every <label> and verify it focuses the associated input.
  • Submit the form with empty required fields and confirm that error messages are announced by screen readers and that focus moves to the error summary or the first invalid field.
  • Verify every input has an accessible name in the browser's accessibility tree.
  • Tab through the entire form and confirm the order is logical.
  • Test with browser autofill to verify autocomplete attributes work correctly.
  • Test with voice control software (Dragon, Voice Control) to confirm labels are speakable targets.

Best Practices

  • Use native HTML form elements (<input>, <select>, <textarea>) with visible <label> elements as the default approach.
  • Set the correct autocomplete attribute on every input to assist users and password managers.
  • Validate on submit rather than on every keystroke to avoid overwhelming screen reader users with premature error announcements.

Common Pitfalls

  • Using placeholder as a substitute for <label>—the placeholder disappears when the user types, leaving no visible label.
  • Announcing errors on every keystroke with aria-live, which creates a barrage of interruptions for screen reader users before they finish typing.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add accessibility-skills

Get CLI access →