IDs that labels and ARIA point to need to be unique
- Rule:
axe/duplicate-id-aria - Checks: IDs used in ARIA and labels must be unique
- WCAG: 4.1.2
- Fix class:
mechanical: A fix needs no authored text or visual decision.
Who it affects
Section titled “Who it affects”- People who use a screen reader or a braille display, most of them blind.
- People who control the page by speaking.
How to fix it
Section titled “How to fix it”A component that renders more than once uses a fixed id in <label for>, aria-labelledby or aria-describedby
Section titled “A component that renders more than once uses a fixed id in <label for>, aria-labelledby or aria-describedby”Generate an id per instance and use it in both places. A reference to a repeated id finds the first element on the page, so a second card can take the first card’s name, and a second field can be left with no label at all.
<label for="shipping-email">Shipping email</label><input id="shipping-email" type="email" autocomplete="email"><label for="billing-email">Billing email</label><input id="billing-email" type="email" autocomplete="email">A hidden copy of the referenced element comes first, such as a closed menu for small screens
Section titled “A hidden copy of the referenced element comes first, such as a closed menu for small screens”Make each copy’s ids unique. Hidden elements still match, so a name can come from text no one sees.
axe asks for review, as it does for every duplicate this rule finds
Section titled “axe asks for review, as it does for every duplicate this rule finds”Check each element that refers to the id in the browser’s accessibility inspector: does it get the name or description it should? Then make the ids unique anyway, since any change to the page’s order can switch which element wins.
Fixes that fail
Section titled “Fixes that fail”Renaming one duplicate without updating its references
Section titled “Renaming one duplicate without updating its references”The label or ARIA attribute that pointed at it now finds nothing, so a field or region loses its name.
In React
Section titled “In React”The part of the fix that differs in React, in its syntax. The rest of the guidance holds.
A field or card component hard-codes the id that a label or ARIA attribute refers to
Section titled “A field or card component hard-codes the id that a label or ARIA attribute refers to”Call useId once in the component and derive each id from it, such as `${id}-hint`, then use the same values in htmlFor, aria-labelledby and aria-describedby.
import { useId } from 'react';
function EmailField({ label, hint }) { const id = useId(); return ( <> <label htmlFor={id}>{label}</label> <input id={id} type="email" autoComplete="email" aria-describedby={`${id}-hint`} /> <p id={`${id}-hint`}>{hint}</p> </> );}Several React roots on one page produce the same useId values
Section titled “Several React roots on one page produce the same useId values”Give each root its own identifierPrefix: in createRoot for a root rendered only in the browser, and the same value in both the server rendering call and hydrateRoot for a server-rendered one, or the ids will not match on hydration.
Fixes that fail in React
Section titled “Fixes that fail in React”Generating the id with Math.random() or Date.now()
Section titled “Generating the id with Math.random() or Date.now()”The value differs between the server and the client, which causes a hydration mismatch, and it changes on every render, which breaks any reference that kept an earlier value. useId stays stable.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.