Hydration Proof

Search documentation

Find a page or section

HP3003: Duplicate id attribute

HP3003 (duplicate-id) means several elements on the page share one id, so labels and ARIA references find only the first. Generate the ids with useId.

HP3003 (duplicate-id) means several elements on the hydrated page share the same id. Labels, ARIA references and getElementById then find only the first one. It is an info finding, not a hydration mismatch: give every element a unique id, and generate the ids of repeated components with useId.

CodeHP3003
Nameduplicate-id
Default severityInfo
GroupMarkup the browser repaired
What it meansSeveral elements share the same id.

What the HP3003 duplicate id finding means

After the page settles, hydration-proof checks the whole document for ids that appear more than once:

2 elements (<input>) have id="email". Labels, ARIA references and getElementById find only the first one.

A few details of the check:

  • Ids inside <template> elements and shadow roots are not counted, and neither are elements you ignore with [data-hydration-proof-ignore] or ignore.selectors.
  • At most 20 duplicate ids are reported per page.
  • When the repeated id was generated by React's useId in two separate React roots, the finding is HP3004 instead, with a different fix.

Info findings do not fail the run and are not printed in the terminal list. They appear in the HTML and JSON reports.

Likely causes

  • A component with a hard-coded id used twice on a page, such as a newsletter form in both the header and the footer.
  • Responsive layouts that render the desktop and the mobile version of the same navigation and hide one with CSS.
  • SVG icons with fixed ids in <defs> (<linearGradient id="gradient">), rendered once per icon.
  • Content from a CMS that repeats anchor ids.

How to fix it

Give every element a unique id; generate ids for repeated components with useId. It returns the same value on the server and during hydration, so it does not create a mismatch.

components/newsletter-form.tsx
// Before: every instance renders id="email"
export function NewsletterForm() {
  return (
    <form>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" name="email" />
    </form>
  );
}
components/newsletter-form.tsx
import { useId } from "react";
 
// After: each instance gets its own id
export function NewsletterForm() {
  const id = useId();
  return (
    <form>
      <label htmlFor={`${id}-email`}>Email</label>
      <input id={`${id}-email`} type="email" name="email" />
    </form>
  );
}

The same works for SVG definitions:

components/logo.tsx
import { useId } from "react";
 
export function Logo() {
  const gradientId = useId();
  return (
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <defs>
        <linearGradient id={gradientId}>
          <stop offset="0" stopColor="#0ea5e9" />
          <stop offset="1" stopColor="#6366f1" />
        </linearGradient>
      </defs>
      <circle cx="12" cy="12" r="10" fill={`url(#${gradientId})`} />
    </svg>
  );
}

Do not replace a hard-coded id with a counter or a random value: the server and the browser would generate different ids, which turns this info finding into a real hydration mismatch.

Catch it with ESLint

no-unstable-id reports ids built from Math.random(), Date.now() or module-level counters, the usual wrong fix for a duplicate id. A hard-coded id repeated by two instances of a component is only visible on the rendered page.

When the duplicate is intentional

Add an ignore.issues rule with code: "HP3003", the element's selector and a reason (see ignoring findings). checks.invalidHtml: false turns off every HP3xxx check, including this one.