Hydration Proof

Search documentation

Find a page or section

Fix the invalid HTML nesting hydration error

React renders what you wrote. The browser parses what HTML allows.

An invalid HTML nesting hydration error means the browser changed the server markup while parsing it. A <div> inside a <p>, for example, does not stay inside that paragraph. React expects the original tree and fails to match the DOM. Fix the nesting in your components.

Symptoms

In development, React may name the elements involved:

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 may show only a generic error. See the message guides for div cannot be a descendant of p and validateDOMNesting.

hydration-proof compares the server HTML with the parsed DOM. It reports invalid nesting as HP3001, or nested interactive elements as HP3002, and points to the server markup.

Why an invalid HTML nesting hydration error happens

The browser follows HTML parsing rules, even when the server sends invalid nesting. For example:

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

React expects the <div> inside the paragraph, but the browser placed it outside. Other common repairs are:

You renderThe browser buildsFix
<div> (or <ul>, <h2>, <table>, another <p>) inside <p>Closes the <p> before the block, and adds an empty <p> after itUse <span> inside, or make the outer element a <div>
<a> inside <a>Closes the outer link before the inner oneMove the inner link out
<button> inside <button>, <form> inside <form>Closes the outer button; ignores the inner form tagMake them siblings
<tr> directly inside <table>Adds a <tbody> around the rowsRender 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

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

Check what child components actually render. <p><Card /></p> is invalid if Card returns a <div>. Markdown or CMS content can create the same problem.

A button should not sit inside a link. Make them siblings. If the entire card needs to be clickable, you can stretch the link over it with CSS:

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

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

Keep text nodes, including {" "}, out of <table>, <tbody>, and <tr>.

Catch it with ESLint

no-invalid-interactive-nesting flags patterns it can see in one component. It also checks Server Components, whose HTML still has to be valid.

npm install -D eslint-plugin-hydration-proof

A linter may not see nesting across components, such as Card returning a <div> inside a parent <p>.

Catch it in CI

hydration-proof test checks the parsed HTML, so it can find problems across components and in CMS content. It reports HP3001 or HP3002 with the affected element. No probe is needed: the parser changed the HTML. See how it works.

npx hydration-proof test