Form errors point at elements that do not exist, so nothing announces them #114

Closed
opened 2026-09-07 18:54:30 +00:00 by tiagoagueda · 0 comments
Owner

What was measured

A company form submitted with an empty name, then every aria-describedby on the page
resolved against every id on the page:

aria-describedby ids referenced:        4
of those, elements that do not exist:   4
examples: id_name_error, id_careers_url_helptext,
          id_new_industries_helptext, id_identifiers-0-label_helptext
aria-invalid present: True
error text present:   True

Four references, four dangling. Every one.

Why

Django renders the widget, and Django is doing the right thing:

<input type="text" name="name" required aria-invalid="true"
       aria-describedby="id_name_helptext id_name_error" id="id_name">

Postulo's own field partial then renders the help and the error without the ids the input
points at
:

{% if field.help_text %}<p class="field-help">{{ field.help_text }}</p>{% endif %}
{% for error in field.errors %}<p class="field-error">{{ error }}</p>{% endfor %}

So the field announces I am invalid, and I am described by two elements -- and neither
element exists. A screen reader gives the invalid state and then has nothing to read. The
error is on the page, in red, two lines down, and unreachable programmatically.

108 uses across 34 templates. Every form Postulo draws itself.

The part that makes it tier 1

The application already knows how to do this. allauth/elements/fields.html says so in a
comment of its own:

here -- and Django's own aria-describedby and aria-invalid come with it.

So allauth's pages -- sign-in, password, email -- are correct, and Postulo's own are not.
Half the application honours the promise and half breaks it, which is worse than a
consistent omission: somebody testing the sign-in flow with a screen reader would conclude
the application was fine.

It is SC 1.3.1 Info and Relationships and SC 3.3.1 Error Identification, both level
A -- below the AA the README commits to.

Why axe passes

axe cannot know that a <p> two lines below an input was meant to describe it, and it does
not report a dangling aria-describedby. This needed the ids resolved against the document,
which is four lines and is the check worth keeping afterwards.

The likely fix, not done here

Give the two paragraphs the ids Django already expects -- {{ field.auto_id }}_helptext and
{{ field.auto_id }}_error -- in partials/field.html, and check the other partials that
draw fields by hand. The error paragraph probably also wants role="alert", as allauth's
does, so it is announced when the page comes back rather than when somebody happens upon it.

Classification

Accessibility, bug. Tier 1: a stated commitment broken on every form, and the fix is two
attributes.

## What was measured A company form submitted with an empty name, then every `aria-describedby` on the page resolved against every `id` on the page: ``` aria-describedby ids referenced: 4 of those, elements that do not exist: 4 examples: id_name_error, id_careers_url_helptext, id_new_industries_helptext, id_identifiers-0-label_helptext aria-invalid present: True error text present: True ``` **Four references, four dangling.** Every one. ## Why Django renders the widget, and Django is doing the right thing: ```html <input type="text" name="name" required aria-invalid="true" aria-describedby="id_name_helptext id_name_error" id="id_name"> ``` Postulo's own field partial then renders the help and the error **without the ids the input points at**: ```django {% if field.help_text %}<p class="field-help">{{ field.help_text }}</p>{% endif %} {% for error in field.errors %}<p class="field-error">{{ error }}</p>{% endfor %} ``` So the field announces *I am invalid, and I am described by two elements* -- and neither element exists. A screen reader gives the invalid state and then has nothing to read. The error is on the page, in red, two lines down, and unreachable programmatically. **108 uses across 34 templates.** Every form Postulo draws itself. ## The part that makes it tier 1 The application already knows how to do this. `allauth/elements/fields.html` says so in a comment of its own: > here -- and Django's own aria-describedby and aria-invalid come with it. So allauth's pages -- sign-in, password, email -- are correct, and Postulo's own are not. Half the application honours the promise and half breaks it, which is worse than a consistent omission: somebody testing the sign-in flow with a screen reader would conclude the application was fine. It is **SC 1.3.1 Info and Relationships** and **SC 3.3.1 Error Identification**, both level **A** -- below the AA the README commits to. ## Why axe passes axe cannot know that a `<p>` two lines below an input was meant to describe it, and it does not report a dangling `aria-describedby`. This needed the ids resolved against the document, which is four lines and is the check worth keeping afterwards. ## The likely fix, not done here Give the two paragraphs the ids Django already expects -- `{{ field.auto_id }}_helptext` and `{{ field.auto_id }}_error` -- in `partials/field.html`, and check the other partials that draw fields by hand. The error paragraph probably also wants `role="alert"`, as allauth's does, so it is announced when the page comes back rather than when somebody happens upon it. ## Classification Accessibility, bug. Tier 1: a stated commitment broken on every form, and the fix is two attributes.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 18:54:30 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#114
No description provided.