# no-invalid-interactive-nesting

> When you nest a button inside button, React markup gets repaired by the browser before hydration. This rule reports that, a div in a p and table rows.

Source: https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting
Last updated: 2026-09-18

When you nest a button inside button, React renders the tree as written, but
the browser's HTML parser repairs the invalid nesting when it reads the
server HTML, so the DOM React hydrates is not the one it rendered.
`no-invalid-interactive-nesting` reports `<button>` in `<button>`, `<a>` in
`<a>`, `<div>` in `<p>`, table rows outside `<tbody>` and similar nesting.

| | |
| --- | --- |
| Rule | `hydration-proof/no-invalid-interactive-nesting` |
| What it reports | Disallow HTML nesting that the browser repairs while parsing, such as <div> in <p> or <a> in <a> |
| recommended / next | Error |
| strict | Error |
| Server Components | Checked |
| Suggestions | No |
| Options | none |

## What it reports

Nesting in JSX that is visible in one component (through fragments, `&&`,
`? :`, arrays and `.map()` callbacks, but not through other components):

- `<a>` in `<a>`, `<button>` in `<button>`, `<form>` in `<form>`, `<label>`
  in `<label>`, at any depth;
- interactive content inside `<a>` or `<button>`: `a`, `button`, `select`,
  `textarea`, `label`, `details`, `iframe`, `embed`, `input` (unless
  `type="hidden"`; a dynamic `type` is not reported) and `audio`/`video` with
  `controls`;
- elements that close an open `<p>` inside a `<p>`: `div`, `p`, `ul`, `ol`,
  `li`, `dl`, `dd`, `dt`, `table`, `h1`–`h6`, `section`, `article`, `header`,
  `footer`, `nav`, `aside`, `main`, `form`, `pre`, `blockquote`, `hr`,
  `figure`, `figcaption`, `fieldset`, `address`, `details`, `summary`,
  `dialog`, `hgroup`, `menu`, `search` and a few legacy tags. As in the HTML
  parser, the search stops at `button`, `table`, `td`, `th`, `caption`,
  `object` and `template`;
- `<tr>` as a direct child of `<table>` (the browser adds `<tbody>`), and
  `<td>`/`<th>` as a direct child of `<table>`, `<tbody>`, `<thead>` or
  `<tfoot>` (the browser adds `<tr>`).

The report is on the inner element. Elements inside `<svg>` and `<math>` are
skipped, and so is `<li>` outside a list: a component boundary may put it in
the right place.

Unlike most rules, this rule also checks
[Server Components](https://hydration.jscrate.dev/docs/eslint#server-components): React hydrates the
elements a Server Component renders, so invalid nesting there breaks
hydration too.

## What happens to a button inside button: React vs the HTML parser

React renders the tree as written, but the browser builds the DOM from the
server's HTML with the HTML parser, which repairs invalid nesting:

```text
server HTML:  <p>Intro<div>Details</div></p>
browser DOM:  <p>Intro</p><div>Details</div><p></p>
```

When React hydrates, the DOM no longer has the structure it rendered. React
reports the problem and renders the page again on the client:

```text
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.
```

[div cannot be a descendant of p](https://hydration.jscrate.dev/docs/errors/div-cannot-be-a-descendant-of-p)
explains that message. `hydration-proof test` reports the same problem as
[HP3001](https://hydration.jscrate.dev/docs/issues/hp3001) (invalid nesting) or
[HP3002](https://hydration.jscrate.dev/docs/issues/hp3002) (nested interactive elements), with the line in
the server HTML.

## Incorrect

```jsx
function Intro() {
  return (
    <p>
      Welcome
      <div className="details">Read more below.</div>
    </p>
  );
}

function CardLink({ href, onSave }) {
  return (
    <a href={href}>
      <h3>Title</h3>
      <button onClick={onSave}>Save</button>
    </a>
  );
}

function Rows({ rows }) {
  return (
    <table>
      {rows.map((row) => (
        <tr key={row.id}>
          <td>{row.name}</td>
        </tr>
      ))}
    </table>
  );
}
```

The rule checks Server Components too, so the same mistake in an App Router
page is reported with the `next` preset:

```jsx title="app/pricing/page.jsx" filename="app/pricing/page.jsx" preset="next"
export default function Page() {
  return (
    <button type="button">
      Compare plans
      <button type="button">Details</button>
    </button>
  );
}
```

## Correct

```jsx
function Intro() {
  return (
    <div>
      <p>Welcome</p>
      <div className="details">Read more below.</div>
    </div>
  );
}

function CardLink({ href, onSave }) {
  return (
    <div className="card">
      <a href={href}>
        <h3>Title</h3>
      </a>
      <button onClick={onSave}>Save</button>
    </div>
  );
}

function Rows({ rows }) {
  return (
    <table>
      <tbody>
        {rows.map((row) => (
          <tr key={row.id}>
            <td>{row.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

## Options

This rule has no options.

## Messages

What ESLint prints for this rule, word for word:

- `<<child>>` cannot be inside another `<<parent>>`. The browser repairs this HTML while parsing the server response, so the page no longer has the tree React rendered and hydration fails. Make them siblings or use a different element.
- Interactive `<<child>>` cannot be inside `<<parent>>`. The HTML is invalid, React reports it as a hydration error, and browsers handle clicks on it inconsistently. Move it outside the `<<parent>>`.
- `<<child>>` cannot be inside `<p>`. The browser closes the `<p>` before the `<<child>>` while parsing the server HTML, so the DOM no longer matches what React rendered and hydration fails. Use a `<div>` instead of the `<p>`, or a `<span>` instead of the `<<child>>`.
- `<tr>` cannot be a direct child of `<table>`. The browser inserts a `<tbody>` while parsing the server HTML, so the DOM no longer matches what React rendered. Wrap the rows in `<tbody>`, `<thead>` or `<tfoot>`.
- `<<child>>` must be inside a `<tr>`. The browser inserts a `<tr>` while parsing the server HTML, so the DOM no longer matches what React rendered. Wrap the cells in `<tr>`.

## When not to use it

When the HTML is never parsed by a browser before React takes over (pure
client rendering). Even then the markup is invalid, so keeping the rule is
recommended.

## Related

Nesting that crosses component boundaries (`<p><Card /></p>` where `Card`
renders a `<div>`) cannot be seen by a linter. `hydration-proof test` finds it
in the real server HTML.

- [Invalid HTML nesting](https://hydration.jscrate.dev/docs/causes/invalid-html): the cause and its fixes
- [validateDOMNesting warnings](https://hydration.jscrate.dev/docs/errors/validatedomnesting), React's
  development warning for the same nesting
- [eslint-plugin-validate-jsx-nesting compared](https://hydration.jscrate.dev/docs/compare/eslint-plugin-validate-jsx-nesting)
- [HP3002: nested interactive elements](https://hydration.jscrate.dev/docs/issues/hp3002)
