# HP3001: Invalid HTML nesting

> HP3001 (invalid-nesting) means the server HTML nests elements in a way HTML forbids, such as a div in a p. Why the browser repairs it and how to fix it.

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

HP3001 (`invalid-nesting`) means the server HTML nests elements in a way HTML
does not allow, such as a `<div>` inside a `<p>`. The browser repairs the
markup while it parses it, so the DOM React hydrates no longer matches what
React rendered. Fix the nesting in the element the finding points at.

| | |
| --- | --- |
| Code | `HP3001` |
| Name | `invalid-nesting` |
| Default severity | Error |
| Group | Markup the browser repaired |
| What it means | The markup nests elements in a way HTML does not allow. Browsers repair it while parsing, so the DOM no longer matches what React rendered. |

## What the HP3001 invalid nesting finding means

hydration-proof reads the server HTML as React wrote it and compares it with
the tree the browser's own parser builds from the same bytes. Every element is
checked with React's own nesting rules, so the finding covers exactly the cases
React warns about in development. It names both elements and the position in
the server HTML:

```text
<div> cannot be a descendant of <p id="intro"> (line 1, column 2317). The browser moved it while parsing, so React cannot hydrate this markup.
```

The HTML parser closes an open `<p>` as soon as a block element starts, and it
adds a `<tbody>` around rows placed directly in a `<table>`:

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

When React hydrates, the nodes are no longer where it rendered them. React
reports a hydration error and renders that part of the page again on the
client, which usually shows up next to this finding as
[HP1010](https://hydration.jscrate.dev/docs/issues/hp1010) or [HP1011](https://hydration.jscrate.dev/docs/issues/hp1011).

The finding is an error when the browser actually moved the element. When the
markup breaks the rules but this browser kept the nesting, it is a warning. A
link in a link, a button in a button or a form in a form is reported as
[HP3002](https://hydration.jscrate.dev/docs/issues/hp3002) instead.

## The React error it corresponds to

React logs the same problem in development builds only. hydration-proof reads
the server HTML, so it finds the nesting in production builds as well:

```text
In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error.
validateDOMNesting(...): <div> cannot appear as a descendant of <p>
In HTML, <tr> cannot be a child of <table>. 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)
and [validateDOMNesting](https://hydration.jscrate.dev/docs/errors/validatedomnesting) explain each message.

## Likely causes

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

- A block element inside a `<p>`: `<div>`, `<ul>`, `<ol>`, `<table>`,
  `<section>`, `<form>`, `<pre>`, `<blockquote>`, `<figure>` or a heading.
- A component that renders a `<div>`, used inside a `<p>`:
  `<p><Card /></p>`. Neither file looks wrong on its own.
- Table markup without its implied wrappers: `<tr>` directly in `<table>`,
  `<td>` directly in `<tbody>`, or a `<div>` between rows.
- A heading inside another heading, or a `<li>` inside another `<li>`.
- A second `<html>` or `<body>` rendered by a nested layout.

## How to fix it

1. Open the finding. It gives the element, the line and column in the server
   HTML and, in a development build, the source line of the component.
2. Change one of the two elements so the nesting is valid: use `<span>` instead
   of `<div>` inside a `<p>`, or turn the outer `<p>` into a `<div>`.
3. Wrap table rows in `<tbody>` and cells in `<tr>`.
4. Check components rendered inside a `<p>`. The problem is often one level
   down, in a component that renders a block element.

A `<div>` inside a `<p>`:

```tsx title="components/intro.tsx"
// Before: the browser closes the <p> when the <div> starts
export function Intro() {
  return (
    <p id="intro">
      Welcome back.
      <div className="details">Read the changelog below.</div>
    </p>
  );
}
```

```tsx title="components/intro.tsx"
// After: the block element is a sibling of the paragraph
export function Intro() {
  return (
    <div id="intro">
      <p>Welcome back.</p>
      <div className="details">Read the changelog below.</div>
    </div>
  );
}
```

Rows without a `<tbody>`:

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

## Catch it with ESLint

[`no-invalid-interactive-nesting`](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
reports block elements inside `<p>`, rows directly in `<table>` and nested
interactive elements while you type, in Server Components too. It sees nesting
inside one component only: `<p><Card /></p>` where `Card` renders a `<div>` is
something only the real server HTML shows.

## Example

```text
  ✖ /about 842ms  1 error
    HP3001 Invalid HTML nesting  (invalid html nesting, 97%)
      #intro  in Intro
      <div> cannot be a descendant of <p id="intro"> (line 1, column 2317). The browser moved it while parsing, so React cannot hydrate this markup.
      components/intro.tsx:4:5
      → Fix the nesting so the browser does not rewrite it, for example use <span> instead of <div> inside <p>.
```

## When the markup is not yours

Markup from a CMS or a third-party widget may be out of your hands for a while.
Ignore the finding with a reason and an expiry date, so it does not become
permanent:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  ignore: {
    issues: [
      {
        code: "HP3001",
        route: "/legacy/**",
        reason: "CMS markup, fixed by the new editor",
        expires: "2026-12-31",
      },
    ],
  },
});
```

[Ignoring findings](https://hydration.jscrate.dev/docs/ignoring) covers selectors, attributes and rules.
`checks.invalidHtml: false` turns off every HP3xxx check at once.

## Related

- [HP3002: interactive element nested in another](https://hydration.jscrate.dev/docs/issues/hp3002)
- [Fix invalid HTML nesting](https://hydration.jscrate.dev/docs/causes/invalid-html)
- [The validateDOMNesting warning](https://hydration.jscrate.dev/docs/errors/validatedomnesting)
- [HP1010: React rendered a branch again](https://hydration.jscrate.dev/docs/issues/hp1010)
- [How hydration-proof compares the server HTML](https://hydration.jscrate.dev/docs/how-it-works)
