# 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.

Source: https://hydration.jscrate.dev/docs/issues/hp3003
Last updated: 2026-09-18

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`.

| | |
| --- | --- |
| Code | `HP3003` |
| Name | `duplicate-id` |
| Default severity | Info |
| Group | Markup the browser repaired |
| What it means | Several 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:

```text
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](https://hydration.jscrate.dev/docs/ignoring) 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](https://hydration.jscrate.dev/docs/issues/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](https://hydration.jscrate.dev/docs/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.

```tsx title="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>
  );
}
```

```tsx title="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:

```tsx title="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](https://hydration.jscrate.dev/docs/causes/unstable-id).

## Catch it with ESLint

[`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/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](https://hydration.jscrate.dev/docs/ignoring)). `checks.invalidHtml: false`
turns off every HP3xxx check, including this one.

## Related

- [HP3004: two React roots generate the same ids](https://hydration.jscrate.dev/docs/issues/hp3004)
- [Generated ids that differ between renders](https://hydration.jscrate.dev/docs/causes/unstable-id)
- [The no-unstable-id ESLint rule](https://hydration.jscrate.dev/docs/rules/no-unstable-id)
- [HP3002: nested interactive elements](https://hydration.jscrate.dev/docs/issues/hp3002)
- [Reading the HTML and JSON reports](https://hydration.jscrate.dev/docs/reports)
