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 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
// 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.
Take nested links and buttons apart
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:
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
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-proofA 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