Duplicate ID Errors: Fixing Repeated id Attributes
An id is meant to name exactly one element on a page. When two elements share it, the validator reports "Duplicate ID", and anything that looks the element up by that name, whether a label, a jump link, an ARIA reference, a script or an SVG gradient, quietly picks the first match and ignores the rest.
The page usually looks fine, which is why these errors pile up in themes and components. This guide shows how to list every repeated id, work out which template or loop produced it, generate unique values and update everything that points at them.
The rule, and why browsers do not complain
The HTML standard says an element's id must be unique among all the ids in the element's
tree, must contain at least one character, and must not contain any spaces. That is the whole rule. Unlike
many other validation errors, there is no parser recovery involved: the browser builds both elements
happily, and nothing on screen changes. The problem only shows when something tries to use the id.
The validator reports it as Duplicate ID "newsletter-email" on the second and later occurrences, with a note pointing at the first one. The count matters: a single id repeated twelve times on a product listing is one bug in one loop, not twelve.
What actually breaks
| Thing that uses the id | What happens with a duplicate | Who notices |
|---|---|---|
<label for="..."> | The label is tied to the first matching field only; the second field has no accessible name and clicking its label focuses the wrong box | Screen reader users, anyone tapping a small checkbox label |
aria-labelledby, aria-describedby, aria-controls | The reference resolves to the first element, so a dialog or accordion panel is announced with another one's text | Screen reader users |
Fragment links such as #reviews | The page jumps to the first match, which may be hidden or in a different section | Everyone using a table of contents or "skip to" link |
document.getElementById() and #id selectors in scripts | The script only ever touches the first element; the second button or counter does nothing | Everyone, as a "the second one doesn't work" bug |
SVG url(#id) for gradients, clip paths, masks and <use> | Every icon uses the first definition; if that icon is hidden, the others can lose their fill or disappear | Everyone, often only in one browser or breakpoint |
The form and list attributes | A button or input is associated with the wrong form or datalist | Anyone submitting the form |
CSS written as #promo { … } is the one consumer that keeps working, because selectors
match every element with that id. That is precisely why duplicates survive design reviews.
Where duplicate ids come from
Almost nobody types the same id twice by hand. Duplicates are produced by code that prints a fixed id more than once, and the same few patterns cause nearly all of them.
- Components in a loop. A product card, testimonial or accordion item with a hard-coded
id="add-to-cart"orid="panel"is printed once per item. - Two copies of the navigation. Many themes output a desktop menu and a separate mobile
menu from the same template, so ids such as
menu-item-42orsearch-inputappear in both, one of them hidden with CSS. Hidden elements still count. - The same form twice. A newsletter sign-up in the header and again in the footer, or a search form in the menu and in the page body, repeats every field id.
- Inline SVG icons. An icon exported from a design tool carries internal ids such
as
linearGradient id="a"orclipPath id="clip0". Paste it into a template used five times on a page and you have five of each. - Copied page builder sections. Duplicating a section or widget often copies any custom id typed into its advanced settings.
- Quick-view and modal markup. A product form rendered in the page and again inside a quick-view or quick-add modal repeats its ids.
- Third-party widgets loaded twice. A chat, review or cookie script included by both the theme and a plugin injects its container twice.
- Anchors added in content. An editor adds
id="faq"to a heading, but the theme already usesfaqfor a section wrapper.
Listing every duplicate on a page
The HTML Checker lists duplicates in the served HTML. Ids added later by JavaScript, such as those in a cookie banner or a chat widget, will not be in that source. To see the page as the browser holds it after scripts have run, paste this into the Console panel of your browser's developer tools:
const counts = {};
document.querySelectorAll('[id]').forEach(el => {
counts[el.id] = (counts[el.id] || 0) + 1;
});
console.table(
Object.entries(counts)
.filter(([, n]) => n > 1)
.map(([id, n]) => ({ id, n }))
);
Each row is a repeated id and how many times it appears. To see where they are, run document.querySelectorAll('[id="search-input"]')
with one of the values and hover over each result in the output: the browser highlights the element on
the page.
Deciding which ones matter first
Search the page source for each duplicated value preceded by for=", aria-,
href="#, url(# or getElementById('. A duplicate that something
refers to is causing a real fault today. A duplicate that nothing refers to is still invalid, but it
is harmless until someone adds a reference later, so it can wait.
The fix, step by step
Step 1
List the repeated ids and their counts
Validate one page of each template with the HTML Checker, then run the console snippet on the same pages to catch ids added by scripts. Put every duplicated value in a list with the number of occurrences and the page types where it appears. Values that repeat a predictable number of times, such as once per product on a collection page, point straight at a loop, while values that appear exactly twice usually mean duplicated navigation or forms.
Step 2
Find what refers to each id
For
each value, search the page source and your code for references: for attributes on labels,
aria-labelledby, aria-describedby and aria-controls, fragment
links, SVG url(#…) references, the form attribute, and JavaScript or CSS selectors.
Note them next to the id. These references are what you must update later, and they also tell you which
duplicates are actively breaking a label, a link or a script today.
Step 3
Trace each duplicate to the code that prints it
Search your theme, component library or template files for the exact id string. If it sits inside a loop, a reusable partial, a snippet included in two places or an inline SVG, that is the source. If you cannot find it in your own code, check page builder settings, plugin or app output and third-party scripts. Fix the code that prints the id, not the rendered page, so the correction applies everywhere that component appears.
Step 4
Remove ids that nothing uses
Many duplicated ids exist only because a designer or builder added them out of habit, and nothing refers to them at all. If step 2 found no labels, ARIA references, links or scripts using an id, and your stylesheet does not select it, delete the attribute. Where a style does rely on it, switch that rule to a class. Removing unused ids is the quickest fix and leaves fewer values to keep unique in future.
Step 5
Generate unique ids where one is needed
For ids that are genuinely used, build the value from something unique to each instance:
the loop index, the product or post ID, the section ID supplied by your platform, or a helper that returns
a fresh value on every call. Keep a readable prefix, such as qty- followed by the product
ID. Generate the id once per instance and store it in a variable, so the element and its references use
exactly the same value.
Step 6
Update every reference to the new value
Change each reference found in step 2 to use the same variable as the element: the label's
for, the ARIA attributes, the fragment link and the SVG url(#…). Rewrite scripts
that looked elements up by a fixed id to find them relative to the component instead, for example by
searching inside the card that was clicked. Scripts that relied on the single id will otherwise break
as soon as it becomes unique.
Step 7
Re-test labels, links and scripts
Validate the pages again and re-run the console snippet with menus and modals open. Then test behaviour: click every label and confirm it focuses its own field, follow each in-page link, use each repeated button, and check that icons keep their colours at every breakpoint. Finally, tab through the form with a screen reader running and listen for each field's name, which confirms the label and ARIA references now resolve correctly.
Unique id patterns for common platforms
The principle is the same everywhere: combine a readable prefix with a value that differs per instance, compute it once, and use that one value for the element and all of its references.
Plain HTML with a server-side loop (PHP example)
<?php foreach ( $products as $product ) :
$qty_id = 'qty-' . $product->id; ?>
<label for="<?= $qty_id ?>">Quantity</label>
<input type="number" id="<?= $qty_id ?>" name="qty[<?= $product->id ?>]" min="1" value="1">
<?php endforeach; ?>
WordPress
For markup that has no natural unique value, WordPress provides wp_unique_id( 'prefix-' ),
which returns a new value on each call during the request. Call it once, store it, and echo the stored
value in both places. For posts in a loop, get_the_ID() is often all you need.
Shopify Liquid
Sections get section.id, which differs between two copies of the same section on one
page, and blocks get block.id. Inside a loop, forloop.index or the product's
id make a card unique:
{%- assign form_id = 'product-form-' | append: section.id | append: '-' | append: product.id -%}
<form id="{{ form_id }}" action="/cart/add" method="post">
...
</form>
<button type="submit" form="{{ form_id }}">Add to basket</button>
JavaScript components
In vanilla JavaScript, crypto.randomUUID() gives a unique value when you create an element,
though a counter is shorter and easier to read in the markup. Framework-specific helpers, such as React's
useId, are covered in the React
and Next.js guide.
Inline SVG icons
Either prefix the internal ids per instance, or, better, define each gradient or symbol once in a
hidden sprite near the top of the page and reference it with <use href="#icon-star">.
Many SVG optimisers can strip unused ids or add a prefix per file; make sure the sprite itself is not
hidden with display: none, which can stop gradients rendering in some browsers. Hiding it
with zero width and height and position: absolute is safer.
Edge cases
Duplicated desktop and mobile navigation
The cleanest fix is a single menu that CSS rearranges at each breakpoint. If the theme must print
two, give each copy a prefix, such as desktop- and mobile-, and remove the
ids from menu items entirely if nothing uses them. WordPress menus add ids such as menu-item-42
to each item; when the same menu is output twice, those repeat, and a filter on nav_menu_item_id
can blank them for one location.
Ids that must stay stable
Some ids are part of a contract: fragment links in emails or other sites, analytics triggers, automated tests or a third-party script that looks for a fixed container. Keep the original value on the first or main instance and rename only the extra copies. Search your analytics and tag manager configuration for the value before changing it.
Ids that start with a number
HTML5 allows an id such as 2024-offers, so the validator will not complain. CSS and querySelector
need it escaped, though, as #\32 024-offers, which trips people up. When you are generating
values anyway, start them with a letter.
Case matters
Reviews and reviews are different ids, so they are not duplicates, but a
link to #reviews will not reach an element with id="Reviews". Keep ids lower
case.
Hidden duplicates still count
Elements hidden with display: none, the hidden attribute or a closed modal
are still in the document. A hidden duplicate that comes first will capture labels and links that were
meant for the visible one, which is the most confusing version of this bug.
Preventing duplicates in future
- Never hard-code an id inside a reusable component. Pass it in, or generate it from the instance. Make this part of your code review checklist.
- Prefer classes for styling and data attributes for scripts. A button that scripts
find with
data-action="add-to-cart"relative to its card never needs an id at all. - Test components in pairs. When building a component, render it twice on a test page. Duplicates, and scripts that only work on the first copy, show up immediately.
- Tell editors which ids the theme reserves. A short list of ids the theme already
uses, such as
main,reviewsandfaq, avoids clashes when editors add their own jump links. - Validate after adding sections. Re-run the HTML Checker whenever a new section type, app or plugin goes live, because that is when repeated containers appear.
Duplicate ids and broken label associations often appear together. Once your ids are unique, the form markup guide covers the remaining form errors, and the ARIA errors guide covers references that point to ids which do not exist at all.
1. Do duplicate IDs affect SEO?
Not directly. Search engines do not penalise a page for repeating an id, and the text is still read. The effects are indirect: a fragment link that search results or other sites point to may land on the wrong section, and interactive features such as tabs, accordions or add-to-basket buttons can fail for visitors when scripts pick the wrong element. Accessibility problems caused by broken labels are the bigger cost. Fix duplicates because they cause real faults, not for rankings.
2. Why does only the first button work when I have two on the page?
Because a script is almost certainly finding the button by its id. Methods such as getElementById and querySelector with an id selector return the first matching element only, so the event is attached to the first copy and the second one does nothing when clicked. Give each button a unique id, or better, remove the id and have the script find buttons by a class or data attribute and handle each one relative to its own component.
3. Is it ever acceptable to repeat an id?
No, not in valid HTML. The standard requires ids to be unique within the document, including elements that are hidden. If you want several elements to share a name for styling or scripting, that is exactly what the class attribute is for, and data attributes work well for scripts. The only reason to tolerate a duplicate is output you genuinely cannot edit, such as a third-party widget, and in that case report it to the supplier.
4. My inline SVG icons disappear or change colour. Could duplicate IDs cause that?
Yes, this is a classic symptom. Icons exported from design tools often define gradients, masks or clip paths with short internal ids and then refer to them with url references. When the same icon appears several times, every copy points at the first definition. If that first icon is hidden, for example in a closed mobile menu, some browsers render the others without their fill. Prefix the ids per icon, or define each shape once in a sprite and reference it.
5. Does the id have to match the name attribute on a form field?
No. They do different jobs and can be different. The id identifies the element in the page, so a label's for attribute, ARIA references and scripts can find it, and it must be unique. The name is the key sent to the server when the form is submitted, and radio buttons in one group deliberately share the same name. Many developers make them match for single fields because it is tidy, but that is a convention, not a requirement.
6. How do I fix duplicate IDs created by a plugin or app I cannot edit?
First check the plugin's settings, because many let you set the id of a form or widget, and duplicates often come from placing the same widget twice. If that fails, report the issue to the developer with the page URL and the repeated values. Avoid rewriting the ids with your own script after the page loads, because the plugin's own code may depend on them. If the duplicate breaks a label or a form, consider an alternative plugin or app.
The HTML Checker validates your markup against W3C standards and reports errors and warnings, including duplicate IDs, as well as doctype, encoding, nesting, deprecated elements, ARIA, forms and the language attribute.
Open HTML Checker Fixes →