Search topics
Duplicate ids that point labels at the wrong field
A newsletter form shown in a page section and again in the footer repeats its ids, so the footer's label names, and focuses, the first copy's field.
<input id="email" name="email" type="email"> The problem
Screen reader users reach the footer's email field and hear no label, and a click on that label sends the cursor back up the page to the other form.
A tea shop's home page asks visitors to join its newsletter twice: once in a band halfway down the page, "Join the newsletter", and again in the footer, where many people look for it. Both are the same small form, an "Email" label over a field and a Sign up button, written once as a partial and included in both places. That is sensible: one form to maintain. But the partial writes id="email" on its field and for="email" on its label, so the page now has two elements with the same id.
A screen reader user who reaches the footer and tabs into the field hears "edit text", and nothing that says what to type. If they go back up to the band, its field is read as "Email Email". A sighted visitor who clicks the word "Email" in the footer, as people do to put the cursor in a field, sees the page scroll back up to the band, with the cursor in the band's field. They scroll down again, click the field itself, and it works. Typing and sending the footer form still work; what is broken is the tie between each label and its field.
HTML says an id must be unique in its document, and the reason shows here. A label's for doesn't look for the nearest field with that id: it takes the first element in the page with it. So both labels point at the band's field, which ends up with two labels, and the footer's field has none. The same goes for aria-labelledby, aria-describedby and aria-controls, which also take the first element with each id, so a hint or an error message in a repeated component ends up read on the wrong copy.
Success Criterion 1.3.1 Info and Relationships asks that relationships shown on screen can also be determined in code. On screen, the footer's "Email" sits over the footer's field; in code, it belongs to another field further up the page. Success Criterion 4.1.2 Name, Role, Value asks that every control's name can be determined in code, and the footer's field has no name at all. Chrome and Firefox both give the band's field the name "Email Email" and the footer's field no name. WCAG 2.2 removed 4.1.1 Parsing, which used to fail duplicate ids as such, and explains that many of those issues "will fail Info and Relationships or Name, Role, Value". This is one of them: the duplicate id is the cause, and the missing label is the failure.
The jump happens to anyone who clicks a label, with or without a disability. A placeholder in the footer's field can hide the missing label: browsers use it as the name when nothing else gives one, so a screen reader reads the placeholder, and only the click still shows the fault.
4 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision A screen reader reaches the footer's field and says only "edit text", with no word for what to type. On the section's field it says "Email Email": both labels, since both point there.
- Limited manipulation Voice control users who say "Click Email" can't reach the footer's field: it has no name to say, and the only field with Email in its name is the one higher up the page.
- Limited vision People who use a magnifier often click a label to put the cursor in its field. The footer's label moves the cursor, and the page, up to the section, so they lose their place, and what they type goes into the other form.
- Limited reach and strength People with tremors often click the words of a label rather than the field itself. Here the click takes them up the page, and they have to find their way back down to the footer.
axe's label rule checks that a field has a label pointing at its id, and the footer's field has one, so the rule passes it. axe does notice the shared id, but its duplicate-id-aria rule only ever sends it for review, because a repeated id alone doesn't say which copy a label meant, and its form-field-multiple-labels rule sends both fields for review as having more than one label. EaseWeb's audit reports the footer's field as a finding when two or more labels point at an id that fields share and the later field is left with no label; a shared id used only by aria-labelledby or aria-describedby stays a lead for a person. Tests that render a component once never meet the problem, and a coding assistant asked for a newsletter form may write a fixed id, which is correct until the form is used a second time.
Try it
Scroll to the footer and click the word “Email”, then Tab into each email field.
Where does the click put the cursor? The panel shows what a screen reader says on each field.
Broken Can you fix this?
Loose-leaf teas, blended in small batches
Join the newsletter
New teas and offers, once a month.
This month’s teas
- Morning Assam $14
- Smoked Lapsang $16
- Mint and Nettle $12
Every tea ships in a resealable tin, and every order over $40 ships free.
Screen reader says:
(Tab to an email field)
The fix
Give each copy its own ids, or put fields in their labels.
<section class="signup"> <h2>Join the newsletter</h2> <form class="signup-form" action="/subscribe" method="post"> <label for="email">Email</label> <input id="email" name="email" type="email" autocomplete="email"> <button>Sign up</button> </form></section>…<footer> … <form class="signup-form" action="/subscribe" method="post"> <label for="email">Email</label> <input id="email" name="email" type="email" autocomplete="email"> <button>Sign up</button> </form></footer>What is wrong3 points
- Two fields have
id="email", so both labels go to the first one, in the band. - The footer's field has no label. A screen reader reads it as "edit text", and the band's field as "Email Email".
- Clicking "Email" in the footer puts the cursor in the band's field, and the page scrolls up to it.
An id for each copy
<section class="signup"> <h2>Join the newsletter</h2> <form class="signup-form" action="/subscribe" method="post"> <label for="email-signup">Email</label> <input id="email-signup" name="email" type="email" autocomplete="email"> <button>Sign up</button> </form></section>…<footer> … <form class="signup-form" action="/subscribe" method="post"> <label for="email-footer">Email</label> <input id="email-footer" name="email" type="email" autocomplete="email"> <button>Sign up</button> </form></footer>How it is fixed3 steps
- Give the partial a value for each place it is included, such as
signupandfooter, and add it to every id the partial writes. Each field now has an id of its own. - Point each label's
forat the id in its own copy. A screen reader reads each field as "Email", and a click on either label puts the cursor in the field under it. - Leave
name="email"as it is. The name is what the server receives, and it may repeat; only ids must not.
This fixes the Before and passes 1.3.1 and 4.1.2. In a component framework, let the framework make the id rather than passing a word in: useId() in React and Vue, $props.id() in Svelte, or section.id for a section added through a Shopify theme's JSON templates (a static section rendered twice keeps its file name as its id). In a list, build the id from something unique to each item, such as its product id or its place in the list.
Both pass. An id for each copy keeps the markup and the layout as they are, and it is the one to use when the field also has a hint, an error message or anything else that points at it by id, since those need unique ids anyway. A field inside its label leaves nothing to repeat and nothing to keep in step, which suits a small form like this one, with nothing pointing at the field but its label; it costs a change to the CSS. The two can also be combined: the label around the field, and generated ids for everything else.
The same form in two places is one case, and a component used in a loop is another. A gift-wrap option on every basket item, written with id="gift-wrap", gives every item's checkbox the same id, and a click on any item's "Gift wrap" ticks the first item's box. Each repeated component on a page needs checking the same way.
- a dialog or a drawer the page renders twice, such as one for the phone layout and one for wider screens;
- a mobile and a desktop copy of the same menu, each with
aria-controlspointing at its own panel; - a widget pasted twice from a provider's embed code, whose ids are fixed in the snippet (Mailchimp's embedded form writes
id="mce-EMAIL").
Hiding one copy with CSS doesn't solve it. A copy hidden with display: none is still in the page, and a label still finds it if it comes first; the visible copy is then the one left without a label, and a click on its label does nothing. Render only the copy that is shown, or give each its own ids.
Ids made from a random number, or from a counter, can differ between the server's render and the browser's, which causes hydration errors in frameworks that render on both. That is why React, Vue and Svelte provide their own id functions; use them rather than Math.random().
Two copies of a form given the same aria-label become two form landmarks that sound the same, which is a separate question; see navigation landmarks that all sound the same for naming repeated regions. A label whose for points at an id that doesn't exist at all, such as one renamed in a template, belongs to form fields with missing or mismatched labels, and an aria-labelledby or aria-describedby that points at a missing id to invalid ARIA that misleads screen readers.
Verify the fix
4 checks.
Test with: Screen reader, Mouse
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever a shared component is used in a new place.
Limits
This guide covers ids repeated by a component shown more than once, and what that does to labels. It doesn't cover two different elements that happen to be given the same id by hand, which is fixed by renaming one of them, or a reference to an id that is missing. Whether a repeated form also needs a different visible label in each place, such as "Email for the newsletter", is covered in vague form labels that don't say what to enter. Test the complete journey with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: info-and-relationships (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- WCAG Understanding: parsing (opens in a new tab)
- html.spec.whatwg.org: forms (opens in a new tab)
- html.spec.whatwg.org: dom (opens in a new tab)
- developer.mozilla.org: label (opens in a new tab)
- react.dev: useId (opens in a new tab)
- dequeuniversity.com: duplicate-id-aria (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.