HTML requires the id attribute to be unique within a document. What actually breaks when two elements end up with the same id?
answer
- invalid, but nothing throws
- first in tree order wins
- selectors do not care about uniqueness
- a component rendered twice
- labels and ARIA references resolve to one node
basics
~20 sNothing throws — every lookup that resolves an id to one element silently takes the first in tree order, so getElementById, fragment links and attributes that reference an id all point at the first match while selector matching still matches all of them, which is what hides the bug.
solid answer
~40 sDuplicate ids are invalid HTML, but no browser raises an error, and that is precisely the danger. Anything that resolves an id to a single element takes the **first in tree order**: `document.getElementById`, navigating to `#section`, a `<label for="…">` binding to its control, and ID-reference attributes like `aria-labelledby`, `aria-describedby`, `list` and `form`. Meanwhile selector matching does not care about uniqueness at all — `document.querySelectorAll("#total")` returns every match, and an `#total` style rule paints all of them. So the page looks correct while a label focuses the wrong input, a screen reader announces the wrong name, and an in-page link jumps to the wrong place. The usual cause is a component with a hardcoded id rendered twice, and the fix is to derive ids from a per-instance unique token.
go deeper
Know that an id must be unique in the page and that document.getElementById returns only the first element when it is not, even though nothing errors.
Explain the split: single-element resolution takes the first in tree order, while selector matching returns every element, and name what breaks — label for, ARIA references, fragment links.
Diagnose it from symptoms — a label focusing the wrong field, a screen reader announcing the wrong name — trace it to a component rendered twice, and put a conformance or a11y check in the pipeline.
Own the id strategy across a codebase: per-instance generated ids for reusable components, id reserved for genuine document-wide addresses, and classes or data-* for everything else.
## What the rule actually says The `id` attribute is global — legal on every element — and its value must be **unique within the whole document**. Two more rules are easy to forget: the value must not be the empty string, and it must not contain ASCII whitespace. Beyond that almost anything is allowed, including a leading digit and punctuation, although such ids need escaping when written into a selector. Uniqueness is a *conformance* requirement. A conformance checker reports duplicates as an error; a browser does not. There is no exception, no console warning, no visual difference. That silence is the whole reason this question gets asked. ## The split that causes the bug The DOM has two families of id consumers, and they behave differently on duplicates. **Single-element resolution takes the first match in tree order:** ```html <input id="total" value="a"> <input id="total" value="b"> <label for="total">Total</label> ``` ```js document.getElementById("total").value; // "a" — the second input is unreachable this way ``` Everything in this family follows the same rule: - `document.getElementById(id)` - fragment navigation to `#id`, whether by URL or by an in-page link - `<label for="id">`, which binds to the first labelable element with that id - ID-reference attributes: `aria-labelledby`, `aria-describedby`, `aria-controls`, an input's `list="…"` pointing at a `<datalist>`, and a control's `form="…"` pointing at a `<form>` it sits outside of **Selector matching ignores uniqueness entirely:** ```js document.querySelectorAll("#total").length; // 2 ``` A selector is just an attribute test, so it happily matches every element carrying that id, and a style rule keyed on the id paints all of them. ## Why this is worse than it sounds The two behaviours combine into a bug that looks like it works. Presentation is driven by selectors, so the page renders exactly as designed. Behaviour is driven by single-element resolution, so it quietly attaches to the wrong node. Concretely: - Clicking the visible label focuses or toggles the *other* control, often one scrolled off-screen. - A screen reader computing an accessible name from `aria-labelledby` reads the first matching element's text, so a duplicated widget announces the wrong thing for every instance but the first. - A share link to `#pricing` scrolls to whichever `#pricing` came first in the document. - Test selectors written against the id become order-dependent and flaky. ## Where duplicates come from Almost always from a reusable chunk of markup with an id baked into it — a form partial, a component template, an icon sprite copied twice, an accordion panel included on a page that already has one. Rendering it twice is the whole failure mode. Related: an element whose id changes over the life of the page can leave stale ID references pointing at nothing, which is a different bug with the same smell. ## Getting it right - Give each instance a unique token and build ids from it, so one component renders `field-3-email` rather than `email`. - Reserve `id` for the cases that genuinely need a document-wide address: label association, ARIA references, fragment targets, `form` and `list` wiring. Use classes or `data-*` attributes for hooks that only need to identify a kind of thing. - Run a conformance check or an accessibility audit in CI; duplicate-id is one of the errors both catch reliably and cheaply. If you ever need every element carrying an id-shaped value, that is a sign the value is describing a category rather than an address — a class or data attribute is the right tool.
- Why does a page with duplicate ids often look completely fine?Because presentation runs through selector matching, which matches every element with that id, so all copies are styled identically. Only single-element resolution — getElementById, fragment links, label `for`, ARIA references — collapses to the first match. The visual layer hides the behavioural bug, which is why it survives review and shows up as a keyboard or screen-reader defect.
- Which attributes besides label's for depend on an id resolving to exactly one element?The ARIA ID-reference attributes `aria-labelledby`, `aria-describedby` and `aria-controls`; an input's `list` pointing at a `<datalist>`; and a control's `form` attribute pointing at a form it is not nested inside. Each looks up an element by id, so a duplicate silently redirects the association to the first match in the document.
- What are the other rules on an id value besides uniqueness?It must not be empty and must not contain ASCII whitespace — a space makes it two tokens' worth of trouble and breaks every lookup. Otherwise the value is unrestricted, including leading digits and punctuation, but such values need escaping when placed in a selector, which is a good reason to keep ids simple.
saying these in an interview costs you the question
- Says the browser throws or warns on duplicate ids
- Thinks querySelectorAll('#x') returns only one element
- Believes the last duplicate wins the lookup
- Uses id as a generic styling or test hook
- Assumes duplicate ids are only a validator complaint