sfw/fix
duplicate-id-aria / F77 high

id attribute value must be unique (duplicate ID)

The same id is used more than once, so label, aria-labelledby, and aria-describedby references resolve to only the first match and silently mistarget.

What you see

id attribute value must be unique
Document has multiple elements referenced with ARIA with the same id attribute: email
Element: <input id="email">

What’s actually happening

Two or more elements carry the same id, and at least one of those ids is the target of a <label for>, aria-labelledby, aria-describedby, or aria-controls. ID references in the DOM resolve to the first element in document order, so every reference points at that one node and the rest get nothing. A field ends up with the wrong label, an error message describes the wrong input, or aria-controls toggles the wrong panel. To a screen reader user the form reads as mislabeled or unlabeled, and nothing in the visual UI hints at the breakage.

Common causes

  • A server-side partial, web component, or React component that hardcodes an id (id="email", id="search") is rendered more than once on the page
  • A list or table built in a loop emits the same id for every row instead of appending an index or unique key
  • Copy-pasted markup duplicated a block — including its ids and the for/aria-labelledby that point at them
  • Two independent widgets coincidentally chose the same generic id ("title", "description", "close") and landed on the same page
  • A modal or off-canvas template is cloned into the DOM while a hidden original with identical ids still exists

How to fix it

  1. Find every duplicate and the references pointing at themIn the console run a quick check: const ids={}; document.querySelectorAll('[id]').forEach(e=>{ids[e.id]=(ids[e.id]||0)+1}); console.table(Object.entries(ids).filter(([,n])=>n>1)). Then grep the codebase for for=, aria-labelledby=, aria-describedby=, and aria-controls= using those id values to see what breaks when you rename.
  2. Make ids unique per instanceAnywhere a component or loop can render twice, suffix the id with a stable unique value — the record key, loop index, or a generated id. In React use the useId() hook; in template engines append {{ index }} or the row's primary key. Update the matching for/aria-* attribute in the same place so the pair stays in sync.
  3. Prefer wrapping <label> when the relationship is one-to-oneFor a single control you can wrap it in its <label> (<label>Email <input></label>) and skip the for/id pairing entirely, which removes one source of duplicate-id bugs. Reserve aria-labelledby/describedby for cases where the text lives elsewhere in the DOM.
  4. Don't leave a hidden duplicate in the DOMIf you clone a template into a live region, either remove the original from the document or strip ids off the inert copy. A display:none element with a duplicate id still counts — duplicate-id-aria fires regardless of visibility because the reference resolution doesn't care whether the node is shown.
  5. Re-scan and confirm the label reads correctlyAfter renaming, focus each control with a screen reader and verify it announces the intended label and description. The scanner clears once ids are unique, but only AT confirms the right text reaches the right control.

Stop it recurring

Generate ids programmatically (useId, a counter, or the record key) so no two instances of a component can ever collide.

Related errors