sfw/fix
Broken ARIA reference high

WAVE: Broken ARIA reference

An aria-labelledby or aria-describedby points to an id that doesn't exist on the page, so the reference resolves to nothing.

What you see

WAVE — Errors
Broken ARIA reference
An aria-labelledby or aria-describedby reference exists, but the target for the reference does not exist.
<input aria-labelledby="email-lbl"> ← no element with id="email-lbl"

What’s actually happening

WAVE flags an element whose aria-labelledby or aria-describedby names an id that isn't anywhere in the DOM. ARIA id references resolve at runtime against real elements; when the target is missing, the browser computes no name (or no description) from it, so the attribute does nothing. An input meant to be labeled by aria-labelledby="email-lbl" ends up with no accessible name at all, and a screen reader says "edit, blank" — even though the markup looks fully wired. aria-describedby that's broken is quieter still: the extra help text or error message just never gets announced, and nobody notices it's gone.

Common causes

  • A typo or casing mismatch between the reference and the target id (aria-labelledby="emailLabel" vs id="emaillabel")
  • The element holding the id was removed, renamed, or moved out of the DOM, but the reference pointing at it stayed
  • The target is rendered conditionally (an error message that only mounts on validation failure) so the id is absent until then, and the reference dangles in the meantime
  • aria-labelledby lists several ids and one of them is wrong — WAVE flags the whole reference even though the others resolve
  • The id lives inside a different component, shadow DOM, or iframe, where the reference can't reach it (ARIA id references don't cross shadow or frame boundaries)

How to fix it

  1. Find the dangling reference and its intended targetIn the console: document.querySelectorAll('[aria-labelledby],[aria-describedby]').forEach(el=>['aria-labelledby','aria-describedby'].forEach(a=>(el.getAttribute(a)||'').split(/\s+/).filter(Boolean).forEach(id=>{if(!document.getElementById(id))console.warn(el,a,'-> missing',id)})). It lists every element, which attribute, and which id is unresolved. That tells you whether to fix the id or fix the reference.
  2. Correct the id so the two strings match exactlyARIA id matching is case-sensitive and whitespace-separated. Make the reference and the target identical, character for character. If the target element exists but under a different id, either rename it or update the aria-* value — whichever doesn't break other references already pointing at it.
  3. Add the missing target element if it should existIf the reference was right and the labelling/description element simply isn't there, add it: <span id="email-lbl">Email address</span>. For aria-describedby help text, that span can be visible or visually hidden, but it has to be present in the DOM for the reference to resolve.
  4. Handle conditionally-rendered targetsFor an error message that only appears on validation, don't point aria-describedby at it while it's absent — add the attribute at the same moment you mount the message, and remove it when the message unmounts. Or keep an always-present container with the id and fill its text in and out, so the reference never dangles.
  5. Re-scan and confirm the name/description is readRe-run WAVE — "Broken ARIA reference" should clear. Then with a screen reader, focus the control and confirm it announces the intended label, and (for describedby) that the help or error text is read after the field name. WAVE clearing only proves the id resolves; AT proves the right text reaches the user.

Stop it recurring

Generate paired ids and aria-* references from the same source (a useId hook, a shared key) so a label and its reference can't drift, and never reference an id that only exists conditionally without managing the attribute alongside it.

Related errors