# HP3002: Interactive element nested in another

> HP3002 (nested-interactive) means a link is inside a link, a button inside a button or a form inside a form. How the browser splits them and how to fix it.

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

HP3002 (`nested-interactive`) means the server HTML puts a link inside another
link, a button inside another button or a form inside another form. HTML does
not allow it, so the browser splits the elements while parsing and the DOM no
longer matches React's tree. Move the inner element out of the outer one.

| | |
| --- | --- |
| Code | `HP3002` |
| Name | `nested-interactive` |
| Default severity | Error |
| Group | Markup the browser repaired |
| What it means | Links, buttons and forms cannot contain another element of the same kind. |

## What the HP3002 nested interactive finding means

It is a special case of [HP3001](https://hydration.jscrate.dev/docs/issues/hp3001): the same check of the
server HTML against React's nesting rules, reported under its own code when the
inner element has the same tag as the outer one (`<a>`, `<button>`, `<form>`,
or the legacy `<nobr>`).

The browser repairs each of them differently:

| Nesting                  | What the parser does                                                        |
| ------------------------ | --------------------------------------------------------------------------- |
| `<a>` in `<a>`           | Closes the outer link when the inner one starts, so the two become siblings |
| `<button>` in `<button>` | Closes the outer button, so the inner one moves after it                    |
| `<form>` in `<form>`     | Drops the inner `<form>` tag; its fields belong to the outer form           |

React then hydrates a DOM with a different shape than it rendered. The finding
is an error when the browser moved the element, and a warning when this
browser kept the nesting.

## The React error it corresponds to

In development, React logs the nesting before it reports the hydration error:

```text
In HTML, <a> cannot be a descendant of <a>. This will cause a hydration error.
<a> cannot contain a nested <a>
validateDOMNesting(...): <a> cannot appear as a descendant of <a>
```

The [validateDOMNesting](https://hydration.jscrate.dev/docs/errors/validatedomnesting) page lists the other
variants.

## Likely causes

The cause is [invalid HTML nesting](https://hydration.jscrate.dev/docs/causes/invalid-html), usually one of:

- A card that is one big link and also contains another link or a "Save"
  button.
- A `<Link>` component inside another `<Link>`: both render `<a>`.
- A disclosure or tab button that contains a close or menu button.
- A form component used inside a page that already wraps its content in a
  `<form>`.

## How to fix it

Do not nest links inside links, buttons inside buttons or forms inside forms.

1. **Make the card a container, not a link.** Put the main link on the title
   and stretch it over the card with CSS, and keep the other link or button as
   a sibling.
2. **Put buttons side by side.** A header button and its close button sit next
   to each other in a wrapper element.
3. **Associate controls with a form outside.** A button or input can belong to
   a form it is not inside, with the `form` attribute.

A card with a link inside a link:

```tsx title="components/post-card.tsx"
// Before: the browser closes the outer <a> when the inner one starts
export function PostCard({ post }: { post: Post }) {
  return (
    <a href={`/posts/${post.slug}`} className="card">
      <h3>{post.title}</h3>
      <a href={`/authors/${post.author.slug}`}>{post.author.name}</a>
    </a>
  );
}
```

```tsx title="components/post-card.tsx"
// After: one link per destination; CSS makes the title link cover the card
export function PostCard({ post }: { post: Post }) {
  return (
    <article className="card">
      <h3>
        <a href={`/posts/${post.slug}`} className="card-link">
          {post.title}
        </a>
      </h3>
      <a href={`/authors/${post.author.slug}`} className="card-author">
        {post.author.name}
      </a>
    </article>
  );
}
```

```css title="components/post-card.css"
.card {
  position: relative;
}
.card-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.card-author {
  position: relative;
  z-index: 1;
}
```

A delete button that needs its own form, inside an edit form:

```tsx title="components/edit-item.tsx"
export function EditItem({ id }: { id: string }) {
  return (
    <>
      <form action="/items/save" method="post">
        <input name="title" />
        <button type="submit">Save</button>
        <button type="submit" form={`delete-${id}`}>
          Delete
        </button>
      </form>
      <form id={`delete-${id}`} action={`/items/${id}/delete`} method="post" />
    </>
  );
}
```

## Catch it with ESLint

[`no-invalid-interactive-nesting`](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
reports `<a>` in `<a>`, `<button>` in `<button>`, `<form>` in `<form>` and other
interactive content inside links and buttons, in Server Components as well.
Nesting that crosses a component boundary is only visible in the server HTML,
which is where this finding comes from.

## When you cannot change the markup yet

Add an `ignore.issues` rule with `code: "HP3002"`, a `reason` and an `expires`
date; see [ignoring findings](https://hydration.jscrate.dev/docs/ignoring). The page still hydrates with a
different DOM until the markup is fixed.

## Related

- [HP3001: invalid HTML nesting](https://hydration.jscrate.dev/docs/issues/hp3001)
- [HP3003: duplicate id attribute](https://hydration.jscrate.dev/docs/issues/hp3003)
- [Invalid HTML nesting and its fixes](https://hydration.jscrate.dev/docs/causes/invalid-html)
- [React's validateDOMNesting warnings](https://hydration.jscrate.dev/docs/errors/validatedomnesting)
- [The no-invalid-interactive-nesting rule](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
