# Fix the invalid HTML nesting hydration error

> An invalid HTML nesting hydration error happens when the browser repairs markup, like a div in a p, before React hydrates. Fix the nesting to stop it.

Source: https://hydration.jscrate.dev/docs/causes/invalid-html
Last updated: 2026-09-18

An invalid HTML nesting hydration error happens when the server sends markup
that HTML does not allow, such as a `<div>` inside a `<p>` or a link inside a
link. The browser's parser repairs it while reading the page, so the DOM no
longer has the structure React rendered. Fix the nesting so the parser has
nothing to rewrite.

## Symptoms

React names the elements, in development builds only:

```text
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.
In HTML, <tr> cannot be a child of <table>.
<a> cannot contain a nested <a>.
Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>.
Hydration failed because the server rendered HTML didn't match the client.
```

Production builds show only the generic hydration failure, which is why this
cause is easy to miss there. The React-specific messages have their own pages:
[div cannot be a descendant of p](https://hydration.jscrate.dev/docs/errors/div-cannot-be-a-descendant-of-p)
and [validateDOMNesting](https://hydration.jscrate.dev/docs/errors/validatedomnesting).

hydration-proof reports it as [HP3001](https://hydration.jscrate.dev/docs/issues/hp3001) (invalid nesting)
or [HP3002](https://hydration.jscrate.dev/docs/issues/hp3002) (an interactive element inside another), with
the cause **Invalid HTML nesting** and the line in the server HTML. It compares
the raw server HTML with what the browser's own parser built, so it names this
cause with 97% confidence.

## Why an invalid HTML nesting hydration error happens

React renders the tree exactly as you wrote it. The browser builds the DOM from
the server's HTML with the HTML parser, which follows the HTML rules and fixes
what breaks them:

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

When React hydrates, it expects a `<div>` inside the `<p>` and finds it next to
it instead. The common repairs:

| You render                                                         | The browser builds                                                  | Fix                                                      |
| ------------------------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------- |
| `<div>` (or `<ul>`, `<h2>`, `<table>`, another `<p>`) inside `<p>` | Closes the `<p>` before the block, and adds an empty `<p>` after it | Use `<span>` inside, or make the outer element a `<div>` |
| `<a>` inside `<a>`                                                 | Closes the outer link before the inner one                          | Move the inner link out                                  |
| `<button>` inside `<button>`, `<form>` inside `<form>`             | Closes the outer button; ignores the inner form tag                 | Make them siblings                                       |
| `<tr>` directly inside `<table>`                                   | Adds a `<tbody>` around the rows                                    | Render the `<tbody>` yourself                            |
| `<td>` directly inside `<tbody>` or `<table>`                      | Adds a `<tr>`                                                       | Wrap cells in `<tr>`                                     |

## How to fix it

### Use an inline element inside a paragraph

```tsx title="intro.tsx"
// Before: <p>Welcome<div className="details">Read more below.</div></p>
export function Intro() {
  return (
    <div>
      <p>Welcome</p>
      <div className="details">Read more below.</div>
    </div>
  );
}
```

Watch components that render a block element: `<p><Card /></p>` breaks if
`Card` returns a `<div>`. Markdown renderers and CMS rich text often wrap
content in `<p>`, so a component placed inside that content needs to render
inline elements.

### Take nested links and buttons apart

A card that is a link and also has a button inside is two interactive elements
in one. Make them siblings, and stretch the link over the card with CSS if the
whole card must be clickable:

```tsx title="card-link.tsx"
export function CardLink({
  href,
  onSave,
}: {
  href: string;
  onSave: () => void;
}) {
  return (
    <div className="card relative">
      <a href={href} className="after:absolute after:inset-0">
        <h3>Title</h3>
      </a>
      <button onClick={onSave} className="relative z-10">
        Save
      </button>
    </div>
  );
}
```

### Write the table elements the browser would add

```tsx title="rows.tsx"
export function Rows({ rows }: { rows: { id: string; name: string }[] }) {
  return (
    <table>
      {/* Before: <tr> directly inside <table> */}
      <tbody>
        {rows.map((row) => (
          <tr key={row.id}>
            <td>{row.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

Stray whitespace between table tags is a text node the table cannot hold.
Keep `{" "}` and line-broken text out of `<table>`, `<tbody>` and `<tr>`.

## Catch it with ESLint

[`no-invalid-interactive-nesting`](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
reports every pattern in the table above when it is visible in one component,
through fragments, conditionals and `.map()` callbacks. It checks Server
Components too, because React hydrates the elements they render.

```bash
npm install -D eslint-plugin-hydration-proof
```

Nesting across components (`<p><Card /></p>` where `Card` renders a `<div>`)
is invisible to a linter.

## Catch it in CI

`hydration-proof test` finds nesting across components, in content from a CMS
and in HTML injected with `dangerouslySetInnerHTML`, because it compares the
real server HTML with the DOM the browser's parser built, using React's own
nesting rules. It reports HP3001 or HP3002 with the element and its line in the
server HTML. No probe is needed: the parser's repair is the proof. See
[how it works](https://hydration.jscrate.dev/docs/how-it-works).

```bash
npx hydration-proof test
```

## Related

- [div cannot be a descendant of p](https://hydration.jscrate.dev/docs/errors/div-cannot-be-a-descendant-of-p)
- [validateDOMNesting warnings](https://hydration.jscrate.dev/docs/errors/validatedomnesting)
- [HP3001: invalid HTML nesting](https://hydration.jscrate.dev/docs/issues/hp3001)
- [The no-invalid-interactive-nesting rule](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
- [Compared: eslint-plugin-validate-jsx-nesting](https://hydration.jscrate.dev/docs/compare/eslint-plugin-validate-jsx-nesting)
