Hydration Proof

Search documentation

Find a page or section

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.

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.

CodeHP3001
Nameinvalid-nesting
Default severityError
GroupMarkup the browser repaired
What it meansThe 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:

<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>:

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 or 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 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:

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 and validateDOMNesting explain each message.

Likely causes

The cause is always invalid HTML nesting. 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>:

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>
  );
}
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>:

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

  ✖ /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:

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 covers selectors, attributes and rules. checks.invalidHtml: false turns off every HP3xxx check at once.