---
title: 'Form Accessibility'
source: 'https://academia.sh/en/courses/web-fundamentals-and-html/form-accessibility'
course: 'Web Fundamentals and HTML'
language: en
updated: '2026-08-17T18:09:32+00:00'
license: 'CC BY-SA 4.0'
---

# Form Accessibility

Binding error messages to a field, building an error summary returned from the server, focus management, and the requirements of keyboard use.

At this point the form collects data, validates it, and can accept a file. One question
remains: what does the user see when submission is rejected?

The Built-in Validation lesson noted two limits of the browser's own message: it shows only
one field at a time, and can disappear quickly. An error returned from the server, on the
other hand, arrives as a new document, and the user has to work out which fields need fixing.

This lesson builds error reporting into the document. The structure built needs no script; it
is markup produced by the server, and so it works under every condition.

## What an Error Report Must Meet

There are four requirements, and all four are met with markup.

**Findability.** The user must see where on the page they landed and how many errors there
are. A single red line in the middle of the page tells a user who is not looking at that line
nothing.

**Binding.** Each message must be bound to the field it belongs to through a declaration. Text
placed under a field is visually bound; it is not bound in the accessibility tree.

**Announceability.** When the field receives focus, the error message must be announced too.

**Reachability.** Reaching the message must not require guessing which field the error is in.

It should also be added that color alone is not a report: a red border tells a user who
cannot distinguish the color nothing. The state must also be reported with text.

## The Error Summary Returned From the Server

The script below builds the document the server produces after validation. The input is the
field definitions and the error list; the output is the markup to be sent.

```js
// accessibility.mjs — the error summary produced on the server, and the field-error bonds
const FIELDS = [
  { id: "station", name: "station", label: "Station code", type: "text" },
  { id: "temperature", name: "temperature", label: "Temperature (°C)", type: "number" },
  { id: "email", name: "email", label: "Notification address", type: "email" },
];

const MESSAGES = {
  valueMissing: "This field is required.",
  patternMismatch: "Use lowercase letters and hyphens only.",
  rangeOverflow: "The value can be at most 60.",
  typeMismatch: "Enter a valid email address.",
};

function escapeHtml(text) {
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function renderForm(values, errors) {
  const lines = [];
  if (errors.length > 0) {
    lines.push('<div role="alert" tabindex="-1" id="error-summary">');
    lines.push("  <h2>Form could not be submitted: " + errors.length + " fields need to be fixed</h2>");
    lines.push("  <ul>");
    for (const [fieldId, code] of errors) {
      const field = FIELDS.find((f) => f.id === fieldId);
      lines.push(
        '    <li><a href="#' + field.id + '">' + escapeHtml(field.label) + ": " + escapeHtml(MESSAGES[code]) + "</a></li>",
      );
    }
    lines.push("  </ul>");
    lines.push("</div>");
  }
  lines.push('<form action="/record" method="post" novalidate>');
  for (const field of FIELDS) {
    const error = errors.find(([id]) => id === field.id);
    lines.push('  <p><label for="' + field.id + '">' + escapeHtml(field.label) + "</label>");
    const attrs = [
      'id="' + field.id + '"',
      'name="' + field.name + '"',
      'type="' + field.type + '"',
      'value="' + escapeHtml(values[field.name] ?? "") + '"',
    ];
    if (error) {
      attrs.push('aria-invalid="true"');
      attrs.push('aria-describedby="' + field.id + '-error"');
    }
    lines.push("     <input " + attrs.join(" ") + ">");
    if (error) {
      lines.push(
        '     <span id="' + field.id + '-error" class="error">' + escapeHtml(MESSAGES[error[1]]) + "</span>",
      );
    }
    lines.push("  </p>");
  }
  lines.push('  <p><button type="submit">Submit record</button></p>');
  lines.push("</form>");
  return lines.join("\n");
}

const values = { station: "North Slope", temperature: "95", email: "observer@example.test" };
const errors = [["station", "patternMismatch"], ["temperature", "rangeOverflow"]];
console.log(renderForm(values, errors));
```

```
<div role="alert" tabindex="-1" id="error-summary">
  <h2>Form could not be submitted: 2 fields need to be fixed</h2>
  <ul>
    <li><a href="#station">Station code: Use lowercase letters and hyphens only.</a></li>
    <li><a href="#temperature">Temperature (°C): The value can be at most 60.</a></li>
  </ul>
</div>
<form action="/record" method="post" novalidate>
  <p><label for="station">Station code</label>
     <input id="station" name="station" type="text" value="North Slope" aria-invalid="true" aria-describedby="station-error">
     <span id="station-error" class="error">Use lowercase letters and hyphens only.</span>
  </p>
  <p><label for="temperature">Temperature (°C)</label>
     <input id="temperature" name="temperature" type="number" value="95" aria-invalid="true" aria-describedby="temperature-error">
     <span id="temperature-error" class="error">The value can be at most 60.</span>
  </p>
  <p><label for="email">Notification address</label>
     <input id="email" name="email" type="email" value="observer@example.test">
  </p>
  <p><button type="submit">Submit record</button></p>
</form>
```

The markup produced meets each of the four requirements.

**The summary sits before the form** and states with a heading how many fields need to be
fixed. Sitting at the top of the document ensures the user reaches it without extra
navigation; the `role="alert"` declaration marks the section as a region to be announced
immediately. Whether it is announced while the document loads depends on the implementation,
so the summary does not rely on the announcement alone: it is also findable by its position
and heading.

**Every error is a link**, and its target is the relevant field's id. The link text does not
just say "this field is required" — it also says which field it is; the "understandable when
taken out of context" criterion from the Links lesson applies here too.

**Fields carry `aria-invalid="true"`.** This is an invalidity declaration to the accessibility
tree, and carries information color does not.

**The message is bound to the field with `aria-describedby`.** The mechanism built in the
Label-Field Relationship lesson is used here for the error message: when the field receives
focus, the name is announced, then the type, then the description.

Note that entered values are **preserved**. Even invalid values are written back into the
fields; the user sees what they typed and corrects it. Returning the form emptied destroys the
user's work on long forms.

Escaping is also required: field values come from the user, and as noted in the Tags,
Attributes, and Entities lesson, writing `<`, `&`, and quote characters into the document
unescaped breaks the structure.

## Focus Management

The summary carries a `tabindex="-1"` attribute so that it can be focused programmatically. A
value of negative one keeps the element out of the tab order but still lets it receive focus.

In a document returned from the server, focus starts at the top of the document and the
summary is already there; no extra step is needed. In checks made without reloading the page,
focus must be moved to the summary, or the user stays unaware of the newly added section.

When the link in the summary is clicked, focus goes to the target field. All that is needed
for this is that the field be focusable; input fields already are.

Values of `tabindex` greater than zero are not used. These values move the element to the
front of the tab order and break the order of the rest of the document; the order should come
from the document's source order. When focus order and visual order diverge, a keyboard user
jumps around the screen.

## Reporting Without a Page Reload

If the check is done without reloading the page, a message that appears on screen needs to be
announced. A **live region** is a section whose content is announced by a screen reader when
it changes.

```html
<div role="alert" id="error-summary" tabindex="-1"></div>
```

A `role="alert"` declaration demands an immediate announcement and interrupts the user's
current reading; this suits error reports. For less urgent information — "submitting
record," for instance — a live region that announces without interrupting is used instead.

The live region must exist in the document **beforehand**. In some implementations, the
content of a live region added to the document afterward is not announced; an empty container
is placed beforehand and its content is filled in later.

## Keyboard Use

A form being usable by keyboard is provided automatically as long as nothing is done in the
document to prevent it: the `input`, `select`, `textarea`, and `button` elements are
focusable, tab order is source order, and submission can be made with the enter key while in
one of the fields.

The places where this behavior breaks are places where the language's own elements are
replaced with something else. A button built by adding a click behavior to a `div` element is
not focusable, cannot be pressed by keyboard, and is not announced as a button by a screen
reader. The declarations needed to win these back — focusability, role, key handling — already
exist on the `button` element.

The focus indicator is not removed either. When the focused element has no visible mark, a
keyboard user cannot tell where in the document they are. The indicator can be changed in the
presentation layer; it cannot be removed.

## The Finished Form

The final section of the document developed throughout the course:

```html
<section aria-labelledby="notice">
  <h2 id="notice">Report a Measurement Correction</h2>

  <div role="alert" id="error-summary" tabindex="-1"></div>

  <form action="/measurement/correction" method="post" enctype="multipart/form-data">
    <fieldset>
      <legend>Measurement information</legend>

      <p><label for="station">Station code</label>
         <input id="station" name="station" type="text" value="north-slope"
                required maxlength="20" pattern="[a-z-]+"
                aria-describedby="station-help">
         <span id="station-help">Lowercase letters and hyphens only.</span></p>

      <p><label for="date">Measurement date</label>
         <input id="date" name="date" type="date" required></p>

      <p><label for="value">Measured value (°C)</label>
         <input id="value" name="value" type="number"
                required min="-60" max="60" step="0.1"></p>
    </fieldset>

    <fieldset>
      <legend>Measurement source</legend>
      <p><label><input type="radio" name="source" value="automatic" checked> Automatic</label></p>
      <p><label><input type="radio" name="source" value="manual"> Entered manually</label></p>
    </fieldset>

    <fieldset>
      <legend>Raw record</legend>
      <p><label for="record">Measurement file</label>
         <input id="record" name="record" type="file"
                accept=".csv,text/csv" aria-describedby="record-help">
         <span id="record-help">Comma-separated value file, 5 MB maximum.</span></p>
    </fieldset>

    <p><button type="submit">Submit correction</button></p>
  </form>
</section>
```

Every field has a label, every group has a name, every constraint has a declaration, and every
description has a bond. There is not a single script in the document.

## Summary

- An error report must be findable, bound to its field, announceable, and reachable; color
  alone is not a report.
- An error summary returned from the server sits before the form, states the error count, and
  presents each error as a link bound to its field.
- Fields announce invalidity with `aria-invalid` and the error message with
  `aria-describedby`; entered values are preserved and written escaped.
- `tabindex="-1"` makes an element focusable without putting it in the tab order; values
  greater than zero break the document's focus order.
- Live regions must exist in the document beforehand; the content of a region added afterward
  may not be announced.
- Keyboard use is provided automatically as long as the language's own elements are used; the
  focus indicator can be changed but not removed.

## Course Wrap-Up

This course opened with a single question: what is the response body that comes back from the
server? The response is a document, and the document is turned from bytes into a tree of
nodes.

Four topics built this tree. **How the Web Works** traced the conversion from bytes to
characters, from characters to tokens, from tokens to a tree; it showed the parser's error
recovery rules and how resource loading holds up this chain. **Document Structure** built the
document's skeleton from the root structure to tables, and repeated that every tag is a
structural declaration, not a presentation choice. **Semantic Markup and Media** added a
top-level map to the document and placed images, audio, video, and embedded content in an
accessible way. **Forms** turned the one-way document into a two-way one, and showed through
real bodies how data coming from the user reaches the server.

The measurement-station document followed throughout the course grew from a two-line skeleton
into a whole page: sectioned, tabled, carrying media, and containing a validatable form. There
is not a single presentation decision in the document: nowhere is a color, a measurement, a
position declared.

This absence is deliberate. Of the three layers defined in the first lesson, only the first
has been built. The document is readable and navigable right now when opened in a browser —
because the browser carries default styles for every element. But that appearance is not the
document's decision, it is the browser's.

The next course, **Visual Presentation with CSS**, builds the second layer: the selectors that
decide which rule applies to which element, how conflicting rules are resolved, the box model,
and flow layout. This is the document that course will work on top of — and a well-built
structure is the precondition for good presentation: giving an element style first requires
that element to exist.
