Contextual form errors and ARIA

The Web Content Accessibility Guidelines (WCAG) are – by design – an abstract resource. While they try to cover all things accessibility in the web (and by extension and update, in the digital world) it is a tough resource to wrap your head around. Luckily, there have been projects and teachers that try to help with the understanding of that important but often times legalese document. Among them are great resources like Marcy Sutton-Todd's TestingAccessibility.com, Sara Soudeidan's Practical-Accessibility.today and the great content from A11y-Collective.com, just to name a few (Note: all of the pages mentioned are paid products, but worth the money in my opinion). But W3C's WAI (Web Accessibility Initiative) provides a bunch of helping non-normative resources to get a grasp on WCAG. The "Quick Reference" is such a useful helper, especially if you consider the fact that it is presenting WCAG's techniques in a quick, in-context way. And of course, there are "Tags" (in the "Filter" tab) to approach the Web Content Accessibility Guidelines from more human-digestible keywords such as "carousels", "modals" and "zoom".

A resource that deserves also a little more time in the limelight is "Using ARIA" (find the current, 2018 version here and the latest Editor's Draft from 2021 over there). Like the document's title possibly already spoils, it is an informational documentation to use WAI-ARIA in practice. Since applying ARIA with only half-knowledge is considered dangerous, because it can create new barriers in the first place, a document like this is needed and well appreciated. If you haven't heard of "Using ARIA" as a document before, you might have heard of the "Five Rules of ARIA". That's the place where they originate from and a place where you can study them.

But enough with the introduction: In this blog post, I will try to explain one section of "Using ARIA" even further, namely the one that centers around providing context sensitive error messages in forms and using ARIA to associate error message and faulty field.

The situation is as follows: if a field is considered faulty or in an error state, it should be semantically marked up as such by adding the attribute aria-invalid="true" to the field. In parallel, it would be most useful and actually required by WCAG Success Criterion 3.3.3 to provide a helpful error message on how to resolve the error. Since aria-errormessage is not considered accessibility-supported enough yet to provide the message, the current best practice is to use aria-describedby for that. Without further context, this attribute is used to supply the accessible description to an element. But in the context of error messages and especially in pair with aria-invalid="true" it is used to point to the ID of the error message, making screen readers read it out loud, after they encounter the field and announce its error state.

Example:

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" aria-describedby="emailMissing" aria-required="true" />

  <p id="emailMissing" hidden>The e-mail address is needed, plase supply.</p>

  <button type="sumbit"> Send </button>

</form>

The aforementioned attributes must only exist in the error state at all and in combination. The respective section of "Using ARIA" mentions three possibilities on how to do that:

If you want to associate context sensitive text, such as an error message, you can:

  1. Add the referenced element to the DOM when the error state occurs.
  2. Add the error text as child of the referenced element in the DOM when the error state occurs.
  3. Add the id reference in the DOM to the aria-labelledby/aria-describedby attribute, when the error state occurs.

In the following, I will try to explain each of the quoted options in detail and with code examples. For creating the error state, I'm using a simple "data not supplied, field empty" situation, regardless of browser capabilities (and their advantages or disadvantages).

Adding the whole error message only in error state

In this case – as far as I understand – the error message is injected in the case the system detects something is wrong. Up until that point, a reference to an accessible description exists, but is invalid. The consequence is that the field is not programmatically considered faulty. Note that I both add the "reference target" in form of the actual error paragraph to the document, and also add aria-invalid="true" in the error state to the empty input field as well to be very clear about it.

Before (non-error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" aria-describedby="emailMissing" aria-required="true" />

  <button type="submit">Send</button>

</form>

After (in error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" aria-describedby="emailMissing" aria-required="true" aria-invalid="true" />

  <p id="emailMissing" hidden>The e-mail address is needed, plase supply.</p>

  <button type="submit"> Send </button>

</form>

You can find a working example with the necessary (and commented) JavaScript in this CodePen.

Preparing the error state container, but adding its text content only in error state

In this scenario, the reference between field and accessible description is valid, but because the referenced element has no text content, the accessible description of the field is void.

Once again, I'm using aria-invalid="true" for marking up the error state explicitly. If you look into the working CodePen below, you will also find that I am removing the hidden attribute and change the text content of the prepared error container.

Before (non-error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" aria-describedby="emailMissing" aria-required="true" />

  <p id="emailMissing" hidden></p>

  <button type="submit">Send</button>

</form>

After (in error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" aria-describedby="emailMissing" aria-required="true" aria-invalid="true" />

  <p id="emailMissing">The e-mail address is needed, please supply.</p>

  <button type="submit">Send</button>

</form>

You can find a working example with the necessary (and commented) JavaScript in this CodePen.

Prepare everything, but build the programmatic association once error state occurs

Here, we are preparing the error message and an easy way to build the reference by using an "unsemantic" data-aria-describedby attribute as a source for the real one. When the form is in its error state, the value of our helper is programmatically detected and "put" in the real aria-describedby. This creates the proper reference.

Update 18/8/2023: I got feedback that this is considered the best of the three options. And also that I wrote type="sumbit" everywhere in the submission button HTML. Sorry and thanks, Birkir!

Before (non-error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" data-aria-describedby="emailMissing" aria-required="true" />

  <p id="emailMissing" hidden>The e-mail address is needed, please supply.</p>

  <button type="submit">Send</button>

</form>

After (in error state):

<form method="POST" name="example">
  <p>Fields marked with "*" are mandatory.</p>

  <label for="email">E-Mail*: </label>
  <input id="email" data-aria-describedby="emailMissing" aria-describedby="emailMissing" aria-required="true" aria-invalid="true" />

  <p id="emailMissing">The e-mail address is needed, please supply.</p>

  <button type="submit">Send</button>

</form>

You can find a working example with the necessary (and commented) JavaScript in this CodePen.

Conclusion

I hope I understood everything right and provided an explanatory extension of the paragraph in question. If not, don't be afraid to reach out to me via E-Mail or Mastodon. Also hat-tip to Sonja Weckenmann who motivated me to write about this topic (as my once-a-year blog post ;)).