Skip to content

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.
  • People who use a screen reader or a braille display, most of them blind.
  • People who control the page by speaking.

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.

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.

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.

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.

axe-core’s documentation of the rule

CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.