A workshop booking form adds a second attendee. Clicking the second Email label still focuses the first attendee's email. Both rows look correct because their CSS classes are shared; the duplicated id="email" is the problem.
Locate duplicates in the rendered document
Run this in the page's development console after adding the rows that trigger the problem:
const groups = new Map();
for (const element of document.querySelectorAll('[id]')) {
const matches = groups.get(element.id) ?? [];
matches.push(element);
groups.set(element.id, matches);
}
const duplicates = [...groups]
.filter(([, elements]) => elements.length > 1);
console.table(duplicates.map(([id, elements]) => ({
id,
count: elements.length
})));
Keep the duplicates variable available to inspect the actual element arrays. Searching the original HTML file can miss rows inserted by a framework or script. This scan covers the current document tree; separately inspect an iframe's document or a component's shadow tree when the affected controls live there.
Do not replace ID-based styling with another ID just to hide the symptom. Use a class for styling several equivalent rows. A unique ID identifies a particular element for a relationship or lookup.
Give each attendee a stable reference set
The second row needs its own control and help IDs:
<div class="attendee-row" data-attendee="a17">
<label for="attendee-a17-email">Attendee email</label>
<input id="attendee-a17-email"
name="attendees[a17][email]"
type="email"
aria-describedby="attendee-a17-email-help">
<p id="attendee-a17-email-help">
Send this attendee's joining instructions here.
</p>
</div>
For attendee a18, use attendee-a18-email and attendee-a18-email-help throughout that row. Keep the input's name aligned with the server's expected payload; changing an ID does not require changing the submission contract.
A stable attendee key survives removing or reordering rows. If the application uses array positions in its payload, keep that payload decision separate from DOM identity. Avoid renumbering every ID during a drag operation if external references or application state still rely on the previous values.
In a framework, use its supported unique-ID mechanism for reusable components and follow its server-rendering rules. Generate IDs consistently during rendering rather than appending a new random suffix on every update.
Repair the relationships, then check their targets
For each renamed ID, inspect references such as for, aria-describedby, aria-labelledby, aria-controls, and href="#..." that apply to that component. Some ARIA attributes contain a space-separated list of IDs; update only the intended token. Do not run an unrestricted text replacement across unrelated content.
For the attendee row above, the browser can expose the label's actual associated control:
const row = document.querySelector('[data-attendee="a17"]');
const label = row.querySelector('label');
const input = row.querySelector('input');
console.assert(label.control === input);
console.assert(
document.getElementById(input.getAttribute('aria-describedby'))
=== row.querySelector('p')
);
The second assertion is deliberately for this example's single help ID. A production component with several description IDs must resolve each token separately. MDN's label reference explains why later duplicate matches do not rescue a broken for association.
Exercise the repeat operation that caused the bug
Add two attendees, click each Email label, and confirm that its own input receives focus. Enter different addresses and inspect the submitted names and values. Remove the first attendee, add another, and run the duplicate scan again. Check the help text through the browser's accessibility inspection as well as visually.
The HTML label guide covers ordinary label markup. Here the final result is a repeated component whose references remain correct after insertion, deletion, and reordering. Keep those operations in the regression check; a screenshot of two well-aligned email fields cannot establish that either label points to the right field.