HTMLAccessible Forms

Accessible Forms

Forms are the most common way sighted, keyboard, and screen-reader users interact with a website—and also the easiest part of a site to make inaccessible. A form that "looks fine" visually can be practically unusable for someone navigating with a keyboard or a screen reader if labels, groupings, and error messages aren't wired up correctly. This page covers the concrete HTML patterns that make forms accessible to everyone.

Every Input Needs a Real Label
Placeholder text is not a label. Once a user starts typing, the placeholder disappears, and many screen readers do not reliably announce it. Every form control needs a real, programmatically associated <label>.

There are two ways to associate a label with an input.

label-association.html

HTML
<!-- Method 1: for/id association (most flexible) -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" />

<!-- Method 2: wrapping (implicit association) -->
<label>
  Email address
  <input type="email" name="email" />
</label>
Prefer for/id
The for/id pattern is more robust and easier to style independently. It also lets you click the label from anywhere to focus the associated input, which grows the clickable target—especially helpful for checkboxes and radio buttons.
Placeholder ≠ label
Never rely on the placeholder attribute alone. It has low color contrast by default, vanishes on input, and is inconsistently announced by assistive technology. Always pair it with a real <label>.
Grouping Related Fields with fieldset and legend
When several inputs form a logical group—like a set of radio buttons for a single question, or the fields of a shipping address—wrap them in <fieldset> with a <legend> as its first child. Screen readers announce the legend text before each control in the group, so users don't lose context.

fieldset-legend.html

HTML
<fieldset>
  <legend>Preferred contact method</legend>

  <label>
    <input type="radio" name="contact" value="email" checked />
    Email
  </label>

  <label>
    <input type="radio" name="contact" value="phone" />
    Phone
  </label>

  <label>
    <input type="radio" name="contact" value="sms" />
    Text message
  </label>
</fieldset>

Without the fieldset, a screen reader landing on the second radio button only announces "Phone, radio button, 2 of 3"—with no idea what question it's answering. With the fieldset, it announces "Preferred contact method, Phone, radio button, 2 of 3."

Associating Error Messages with aria-describedby
Visually, an error message placed under an input is obvious. For a screen-reader user tabbing through the form, it's invisible unless it's explicitly linked to the input via aria-describedby. The attribute's value is the id of the element containing the description or error text.

error-association.html

HTML
<label for="password">Password</label>
<input
  type="password"
  id="password"
  name="password"
  aria-describedby="password-error password-hint"
  aria-invalid="true"
/>
<p id="password-hint">Must be at least 8 characters.</p>
<p id="password-error" role="alert">
  Password is too short.
</p>
Now when the input receives focus, the screen reader reads the label, the hint, and the error message together—in the order their IDs are listed. Setting aria-invalid="true" also tells assistive technology the field currently fails validation.
aria-describedby accepts multiple IDs
The value is a space-separated list of element IDs, read in that order. This lets you combine a persistent hint with a conditional error message on the same input.
Announcing Required Fields
The required attribute does double duty: it triggers native browser validation, and it's announced by screen readers as "required" when the field receives focus. Don't rely on a visual asterisk alone—pair it with the attribute (and ideally text, since a bare asterisk can be ambiguous).

required-field.html

HTML
<label for="fullname">
  Full name <span aria-hidden="true">*</span>
  <span class="visually-hidden">(required)</span>
</label>
<input type="text" id="fullname" name="fullname" required />
Here the asterisk is decorative and hidden from assistive technology with aria-hidden="true", while a visually-hidden span supplies the same information as real text for screen readers.
Accessible Custom Form Controls
Native elements (<input>, <select>,<button>) come with keyboard support, focus management, and screen-reader semantics built in for free. When a design calls for a custom-styled control—a toggle switch, a combobox, a star rating—build it from a native element first, then layer on CSS. Reaching straight for a <div> with click handlers means rebuilding keyboard support, focus states, and ARIA roles from scratch.

custom-toggle.html

HTML
<!-- Good: a real checkbox styled to look like a toggle switch -->
<label class="toggle">
  <input type="checkbox" role="switch" name="notifications" />
  <span class="toggle-track" aria-hidden="true"></span>
  Enable notifications
</label>
Avoid div-based controls
A <div> with an onclick handler is not keyboard-focusable, isn't announced as a control, and doesn't respond to Space or Enter without extra ARIA and JavaScript. Start from a native form element whenever one exists for the interaction you need.
Putting It Together

accessible-signup-form.html

HTML
<form>
  <fieldset>
    <legend>Account details</legend>

    <div>
      <label for="signup-email">Email address</label>
      <input
        type="email"
        id="signup-email"
        name="email"
        required
        aria-describedby="email-hint"
      />
      <p id="email-hint">We'll never share your email.</p>
    </div>

    <div>
      <label for="signup-password">Password</label>
      <input
        type="password"
        id="signup-password"
        name="password"
        required
        minlength="8"
        aria-describedby="password-hint"
      />
      <p id="password-hint">At least 8 characters.</p>
    </div>
  </fieldset>

  <button type="submit">Create account</button>
</form>
Quick Reference

Pattern

Purpose

<label for="id">

Programmatically associates label text with a control

<fieldset>/<legend>

Groups related controls and announces group context

aria-describedby

Links hint/error text to an input for screen readers

aria-invalid="true"

Flags a field as currently failing validation

required

Native required-field validation and announcement

Native elements first

Reuse built-in keyboard support instead of reinventing it

  • Every input needs a real, associated <label>—never placeholder text alone.

  • Group related controls with <fieldset> and <legend>.

  • Link error and hint text to inputs with aria-describedby.

  • Prefer native form elements over div-based custom controls.

Test with a keyboard first
Before reaching for a screen reader, unplug your mouse and try to complete the form using only Tab, Shift + Tab, Space, and Enter. If you get stuck, assistive technology users will too.