A password field in a form has a permanent format hint under it, and on a failed submit an error message appears in the same area. How do you wire both to the input in HTML, and what does aria-describedby not do for you?
answer
- name, description and state are separate
- list both IDs, order decides reading
- invalid is a state, not text
- attaching text is not announcing it
- focus has to reach the field
basics
~20 sGive the hint and the error each an id and list both in the input's aria-describedby, in the order you want them read, alongside aria-invalid="true" on failure. aria-describedby only attaches text to the field; it does not announce anything on its own.
solid answer
~50 sGive each piece of text a stable `id` and reference them from the input: `aria-describedby="pw-hint pw-error"`. Both resolve, and their text is concatenated in the order listed, so put the error where you want it heard — usually first, since it is the actionable part. Add `aria-invalid="true"` when the field fails so the control is announced as invalid rather than merely described. Keep the error element in the DOM and empty it when valid, or add and remove its ID from the list — either works, but pick one convention, because a reference to a removed element silently contributes nothing. The important limitation: `aria-describedby` attaches text; it does not announce it. The description is read when focus reaches the field or when the user asks for it. If the error appears while focus is elsewhere — the moment after a submit, say — the user needs a separate announcement mechanism to learn about it; the description alone will sit there unread.
code
html · 7 lines<label for="pw">Password</label>
<input id="pw" name="password" type="password"
autocomplete="new-password"
aria-describedby="pw-error pw-hint"
aria-invalid="true">
<p id="pw-error">Password must contain a number.</p>
<p id="pw-hint">At least 12 characters, including a number.</p>go deeper
Know that hint and error text must be given IDs and referenced from the input via aria-describedby, and that the visible red text alone is not an association.
Explain that several IDs are concatenated in attribute order, that aria-invalid carries the failed state separately from the message, and that both must be updated when the field becomes valid again.
Demonstrate the production judgment: the description is read on focus, not on change, so a submit-time error needs deliberate focus handling or a separate announcement path. Name the dangling-reference and colliding-ID failure modes.
Own the form-error contract across the codebase — one convention for mounting error elements and generating IDs, an agreed submit-failure focus behaviour, and automated checks so each team is not re-inventing error accessibility.
## The wiring ```html <label for="pw">Password</label> <input id="pw" name="password" type="password" aria-describedby="pw-hint" autocomplete="new-password"> <p id="pw-hint">At least 12 characters, including a number.</p> <p id="pw-error"></p> ``` And after a failed validation: ```html <input id="pw" name="password" type="password" aria-describedby="pw-error pw-hint" aria-invalid="true" autocomplete="new-password"> … <p id="pw-error">Password must contain a number.</p> ``` Three things are doing distinct jobs. The `<label for="pw">` supplies the **name** — "Password". `aria-describedby` supplies the **description**, built by concatenating the referenced elements' text in list order. `aria-invalid="true"` flips a **state**, so the control is announced as invalid; without it the error text is just more description and nothing signals that the field is in a failed state. ## Order matters, and it is attribute order Because the pieces are joined in the order the IDs appear, listing the error first means a user arriving at the failed field hears the actionable problem before re-hearing the rules they already know. Listing the hint first is defensible too — what is not defensible is not deciding, and letting the order fall out of whatever your rendering code happened to append. ## Two conventions for the error element, pick one **Keep it mounted, empty its text.** The `<p id="pw-error">` is always in the DOM and always referenced; when the field is valid it holds an empty string and contributes nothing to the description. Simple, and the reference can never dangle. **Mount it and add the ID.** The error element only exists when there is an error, and the ID is added to `aria-describedby` at the same time. This keeps the attribute honest but needs the two updates to stay in lockstep — remove the element while leaving the ID behind and the reference silently resolves to nothing, with no console warning. Either is fine. Mixing them per-component is how you end up with fields that describe an error that is no longer on screen. ## What aria-describedby does not do This is the part interviewers are actually probing. `aria-describedby` is a **static association**, not an event. Changing the referenced text, or adding a new ID to the list, does not cause a screen reader to say anything. The description is surfaced when the user's focus lands on the control, and even then some verbosity settings shorten or skip descriptions entirely. So the submit-time flow needs more than the wiring above. If the user presses Submit and the button keeps focus, nothing about the newly rendered error reaches them: the input's description changed off-screen and unheard. Real forms solve this by moving focus deliberately — to the first invalid field, or to an error summary at the top of the form — so that the description is encountered, or by exposing the message through a mechanism designed to announce changes. Whichever route you take, decide it explicitly rather than assuming the attribute carries it. A second limitation: descriptions are supplementary by design. Anything a user must have to identify or operate the control belongs in the **name**, not the description. A field whose only identifying text sits in `aria-describedby` is a nameless field with a helpful note attached. ## Common ways this ships broken - **The label is in `aria-describedby`.** The control is unnamed; the description sounds like a label but the name field is empty. - **`aria-invalid` is never cleared.** Set it to `false` or remove it when the field passes, or the control is permanently announced as invalid. - **The error text is not associated at all** — it is only visually adjacent, in red. Sighted mouse users see it; nobody navigating by control hears it. - **The error duplicates the label** — "Password: Password must contain a number" — because both are being read in sequence. - **Per-row or per-instance IDs collide.** Two password fields on one page both using `id="pw-error"` means the second field describes the first field's error. ## Verifying it Focus the field with the keyboard and read the computed Description in the browser's accessibility inspector, both before and after triggering the failure. That check catches dangling references, wrong ordering, and the case where the description quietly went empty — none of which are visible in the rendered page.
- Why add aria-invalid when the error text is already referenced by aria-describedby?They carry different information. The description is prose; `aria-invalid="true"` is a machine-readable state, so the control itself is announced as invalid and assistive technology can offer navigation such as jumping between invalid fields. Without it, a user who skips descriptions hears a perfectly ordinary field. Clear it when the value becomes valid, or the field stays permanently marked.
- The user submits, an error renders, and focus stays on the submit button. What have they heard?Nothing. `aria-describedby` associates text; it does not announce it. The description changed on an element the user is not focused on, so it is simply waiting to be encountered. The form has to do something deliberate — move focus to the first invalid field or to an error summary — for the message to reach the user at all.
- Is it better to keep the error element always in the DOM or to add it only on failure?Both are workable; consistency matters more than the choice. Always-mounted with empty text makes dangling references impossible and keeps the attribute static. Conditional mounting keeps the markup honest but requires the ID list to be updated in the same operation. The failure to avoid is a half-migration where the element is removed and its ID lingers in `aria-describedby`.
- Can you put the field's label text in aria-describedby to save markup?No — that leaves the field unnamed. The description slot is supplementary and may be shortened or skipped by verbosity settings, so anything required to identify the control has to be in the name, from a `<label for>`, `aria-labelledby` or `aria-label`. A description without a name is the classic inversion of the two slots.
saying these in an interview costs you the question
- Thinks aria-describedby announces text when it changes
- Puts the field's label into aria-describedby
- Sets aria-invalid on failure and never clears it
- Renders the error visually adjacent but never associates it
- Removes the error element but leaves its ID referenced