Forms are where users do the work that matters — signing up, checking out, submitting a report, booking an appointment. They are also where accessible HTML most frequently breaks down. The gap between a form that looks fine and one that works for everyone using keyboard navigation, a screen reader, or voice input is almost always a gap in markup, not design. This piece covers the patterns that close that gap — and it assumes the form is already using native HTML validation correctly, since accessible error messaging and native constraint validation are complementary, not competing, concerns.

Label Association: Three Methods, One Purpose

Every form control needs a programmatically associated label. “Associated” means the relationship is expressed in the HTML itself, not inferred from visual proximity. There are three reliable methods.

The for/id pair is the most explicit and the most widely supported:

<label for="email">Email address</label>
<input type="email" id="email" name="email" autocomplete="email">

The for attribute on the label must exactly match the id on the input. When they match, clicking the label focuses the input — a usability benefit for everyone, not just assistive technology users. Screen readers announce the label text when the input receives focus.

The wrapping label associates implicitly by containment:

<label>
  Email address
  <input type="email" name="email" autocomplete="email">
</label>

This works and is well-supported, but it constrains your layout options. When the label and input need to be in different DOM positions — a common requirement in complex form grids — the for/id pair is the only option.

aria-labelledby associates a control with any element by its id, regardless of element type or DOM position:

<span id="email-label">Email address</span>
<input type="email" aria-labelledby="email-label" name="email" autocomplete="email">

Reserve aria-labelledby for situations where the visible label is not a <label> element — for instance, when a table column header serves as the label for cells containing inputs, or when you need to concatenate multiple text fragments into a composite label by referencing multiple id values space-separated. Do not use it as a substitute for for/id without a clear reason; for/id is simpler and more robust in AT implementations.

One additional attribute worth knowing: aria-label provides a label as a string value rather than a reference. It is appropriate when no visible label exists and adding one is not feasible — a standalone icon button inside a search input is a typical case. Avoid using aria-label to override or shadow a visible label; when the visible text and the accessible name diverge, voice control users who read the screen and then speak a command will be confused.

Why Placeholder Is Not a Label

The placeholder attribute has a specific, limited purpose: it hints at the expected format of input, not the identity of the field. It fails as a label for several reasons.

First, placeholder text disappears the moment a user begins typing. Anyone who needs to refer back to what the field is asking — users with cognitive disabilities, users who were interrupted mid-form, anyone filling out a long form — loses that information.

Second, placeholder contrast requirements are lower than standard text contrast requirements. The WCAG 1.4.3 minimum contrast ratio of 4.5:1 applies to text, but placeholder text rendered at lower opacity often fails this threshold. Browsers do not consistently flag this, and many design systems ship forms with barely-visible placeholder text.

Third, placeholder text is not reliably announced by screen readers in a consistent way. Some readers announce it as part of the accessible name computation; others do not. The behavior varies by reader and browser combination, making it an unreliable communication channel.

Use placeholder to show an example value or format hint — placeholder="e.g. jane@example.com" — alongside a proper label, never instead of one. See the HTML semantics article for more on how browsers compute accessible names from different sources.

Marking Required Fields

The required attribute on an input serves two functions: it prevents form submission when the field is empty (native browser validation), and it signals to assistive technology that the field is required. Screen readers typically announce “required” when focusing such a field.

<label for="name">
  Full name
  <span aria-hidden="true">*</span>
</label>
<input type="text" id="name" name="name" required autocomplete="name">

The aria-hidden="true" on the asterisk prevents screen readers from reading “asterisk” or “star” — the required attribute already communicates the constraint. Somewhere on the page (typically near the top of the form), include a visible note explaining that asterisk-marked fields are required, for users who can see but may not know the convention.

aria-required="true" is the ARIA equivalent of required. Use it when you are working with custom controls that do not use native HTML form elements — a custom date picker built from <div> elements, for instance. For native inputs, prefer the required attribute; it activates built-in browser validation behavior in addition to the accessibility semantics.

Error Messaging That Actually Works

Native browser validation provides error messages, but they are visually inconsistent across browsers, cannot be styled to match your design system, and are not always announced at the right moment by screen readers. For production forms, custom error messaging is almost always the right call.

Associating Errors with Inputs

The pattern that works most reliably across assistive technology is aria-describedby:

<label for="email">Email address</label>
<input
  type="email"
  id="email"
  name="email"
  aria-describedby="email-error"
  aria-invalid="true"
  autocomplete="email"
>
<span id="email-error" class="error-message">
  Enter a valid email address, such as name@example.com
</span>

aria-describedby associates supplementary description with a control. Unlike aria-labelledby, it does not replace the label — it adds information. Screen readers announce the description after the label and field type, typically after a brief pause. The aria-invalid="true" attribute signals that the current value fails validation; screen readers often announce “invalid” when this is set.

Write error messages in plain language that tells users what to enter, not just that something is wrong. “Required” is marginally useful. “Enter your date of birth in DD/MM/YYYY format” is actionable.

Dynamic Errors and aria-live

When errors appear dynamically — inline validation as a user leaves a field, for instance — the injected error text is not automatically announced by screen readers unless it is inside an aria-live region. The most common approach:

<div aria-live="polite" aria-atomic="true">
  <!-- Error messages injected here by JavaScript -->
</div>

aria-live="polite" announces changes after the user finishes their current interaction. aria-atomic="true" tells the reader to announce the entire region contents when any part changes, rather than just the changed fragment. Use aria-live="assertive" only for critical alerts that cannot wait — it interrupts whatever the reader is currently announcing, which is disruptive if overused.

An important constraint: the aria-live region must exist in the DOM before content is injected into it. Injecting the region itself does not trigger announcement. Build the region into the initial page structure and update its contents dynamically.

Error Summaries for Multi-Field Forms

When a user submits a form and multiple fields fail validation simultaneously, announcing errors field-by-field at each input is not enough. The user at the top of the form has no indication that errors exist further down. The accessible pattern is an error summary: a focusable container that appears (or becomes visible) at the top of the form listing all errors as links.

<div id="error-summary" tabindex="-1" role="alert">
  <h2>There are 3 errors in this form</h2>
  <ul>
    <li><a href="#email">Email address: enter a valid email address</a></li>
    <li><a href="#phone">Phone number: enter a UK phone number</a></li>
    <li><a href="#postcode">Postcode: enter a full postcode</a></li>
  </ul>
</div>

After the form submits and errors are detected, move focus to this summary container with document.getElementById('error-summary').focus(). The tabindex="-1" makes it programmatically focusable without adding it to the natural tab order. The role="alert" triggers announcement in some AT combinations, but focus movement is the more reliable mechanism. Each link in the summary moves focus directly to the offending input, letting users navigate directly to fix each error.

The UK Government Digital Service pattern, documented in their GOV.UK Design System, is the most-tested implementation of this approach and worth studying in detail.

Fieldset and Legend for Grouped Controls

Radio button groups and checkbox groups present a labeling challenge: each control has its own label, but the group also needs a shared label that gives context. A radio group asking “What is your preferred contact method?” with options “Email,” “Phone,” and “Post” — “Email” alone is not a meaningful label without the group context.

The semantic solution is <fieldset> and <legend>:

<fieldset>
  <legend>Preferred contact method</legend>
  <label>
    <input type="radio" name="contact" value="email"> Email
  </label>
  <label>
    <input type="radio" name="contact" value="phone"> Phone
  </label>
  <label>
    <input type="radio" name="contact" value="post"> Post
  </label>
</fieldset>

Screen readers typically announce the legend text before each control’s label as focus moves through the group. A user hears “Preferred contact method, Email, radio button, 1 of 3” — the full context is preserved.

<fieldset> and <legend> also work for checkbox groups and any set of related inputs where a shared context label is needed. Avoid nesting <fieldset> elements more than one level deep; browser and screen reader support for nested fieldsets is inconsistent. If your form structure requires hierarchical grouping, consider whether the hierarchy belongs in the UI at all, or whether it can be flattened.

The autocomplete Attribute

The autocomplete attribute maps form fields to a standardized vocabulary of personal data types. Browsers use it to pre-fill fields from saved profiles; password managers use it to identify credential fields; and users with motor disabilities or cognitive disabilities benefit enormously from not having to re-type information the browser already has.

WCAG 1.3.5 (Identify Input Purpose) requires autocomplete on inputs that collect personal information, as part of the criteria for Level AA conformance. The full list of valid token values is defined in the HTML Living Standard.

Common values worth knowing:

Field purposeautocomplete value
Given (first) namegiven-name
Family (last) namefamily-name
Full namename
Emailemail
Current passwordcurrent-password
New passwordnew-password
Street addressstreet-address
Postal/ZIP codepostal-code
Credit card numbercc-number
One-time codeone-time-code

Two values with specific security implications: new-password tells password managers to generate and save a new credential (appropriate on registration and password-change flows), while current-password signals an existing credential (appropriate on login forms). Using current-password on a registration form’s password field is a semantic mismatch that can confuse password manager behavior.

For WCAG 2.2, the newer success criteria 3.3.7 Redundant Entry and 3.3.8 Accessible Authentication extend this concern: forms should not ask users to re-enter information provided earlier in the same session, and authentication flows should support copy-paste and third-party credential tools. Blocking paste in password fields — a still-common antipattern — directly violates 3.3.8.

Focus Management After Submission and Error Reveal

When a user submits a form and the page response is dynamic — errors revealed in place, a success message injected, a multi-step flow advancing to the next step — focus must be managed explicitly. Without it, focus typically remains on the submit button, which is now in a different state relative to the surrounding content, or is lost entirely.

The rules are straightforward:

  • On error reveal: Move focus to the error summary container (see above). Do not move focus to the first errored field; the summary gives users a complete picture first.
  • On successful single-page submission: Move focus to a confirmation message. If the page navigates to a new URL, the focus reset is handled by the browser’s navigation behavior — but verify that the new page’s <title> reflects the new state (“Order confirmed — Acme Shop”), since screen readers announce the page title on navigation.
  • On multi-step form advance: Move focus to the new step’s heading or to a descriptive container. Users who cannot see the screen have no awareness that content has changed without a focus cue.
  • On modal or panel open: Move focus to the first interactive element inside the newly opened region, or to the container itself if it has tabindex="-1". Trap focus within the modal while it is open; return focus to the trigger element when it closes.

Focus management is one of the most consistently under-implemented aspects of accessible form design. It is also one of the most impactful: a screen reader user who submits a form and hears silence — no error, no confirmation, focus adrift on a button that no longer does anything — has no path forward without exploring the entire page from scratch.

Testing focus behavior requires testing with an actual screen reader (NVDA + Firefox, JAWS + Chrome, and VoiceOver + Safari cover the most significant AT/browser combinations in production). Automated tools like axe-core can catch label-association and ARIA errors, but they cannot verify that focus moves correctly after a dynamic interaction. That requires manual testing.

Building accessible forms is not a checklist exercise applied at the end of a project. Label association, error messaging, grouping, autocomplete, and focus management are design decisions that shape the markup from the beginning. The patterns described here represent a floor — the minimum that makes a form usable across the range of people and tools that encounter it. The ceiling is a form that requires no accommodation at all, because its structure made the right choices from the start.