In HTML, <div> cannot be a descendant of <p>: a paragraph may only
contain text-level elements. When the server sends a <div> inside a <p>,
the browser closes the paragraph before the <div>, so the DOM no longer
matches what React rendered and hydration fails. Use a <span> inside, or
make the outer element a <div>.
The error
React 19 logs this in development:
In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.Under it, React prints the component tree from the <p> down to the <div>,
with both lines marked >. When the two elements are in different components,
it logs a second message, <p> cannot contain a nested <div>., whose stack
points at the component that renders the <p>.
React 18 logs it as a validateDOMNesting warning with a component stack:
Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>.
at div
at p
at Card (webpack-internal:///./components/card.tsx:9:11)The same message appears for every block element inside a paragraph:
In HTML, <p> cannot be a descendant of <p>.
In HTML, <ul> cannot be a descendant of <p>.
In HTML, <ol> cannot be a descendant of <p>.
In HTML, <pre> cannot be a descendant of <p>.
In HTML, <table> cannot be a descendant of <p>.
In HTML, <h2> cannot be a descendant of <p>.These are development warnings, with no production form. What you get in production is the hydration error that follows: #418.
Why div cannot be a descendant of p
A <p> element can only hold phrasing content: text, <span>, <a>,
<strong>, <img> and similar. When the HTML parser meets the start of a
block element, such as <div>, <p>, <ul>, <ol>, <pre>, <table> or a
heading, while a paragraph is open, it closes the paragraph first. The </p>
that comes later has nothing to close, so the parser adds an empty paragraph:
server HTML: <p>Intro<div>Details</div></p>
browser DOM: <p>Intro</p><div>Details</div><p></p>React then hydrates this DOM, expects the <div> inside the <p>, and does
not find it. React 19 throws
"Hydration failed because the server rendered HTML didn't match the client",
and React 18 throws
"Hydration failed because the initial UI does not match…".
Either way the page is rendered again in the browser.
Pages rendered only in the browser do not break, because DOM methods do not repair nesting the way the parser does. That is why the warning says it "will cause a hydration error": server rendering is what exposes it.
Common causes
- A
<div>or a list written inside a<p>in your own JSX. - A component that renders a block element, used inside a paragraph.
<p><Card /></p>looks fine untilCardrenders a<div>. - Component libraries whose text component is a
<p>. MUI's<Typography>renders a<p>for body text by default, and Chakra's<Text>does too. A<Box>(a<div>) or another<Typography>inside it is a<div>or<p>in a<p>. - Markdown renderers. They wrap images in a
<p>. A custom image component that renders a<figure>or a<div>ends up inside it. - HTML from a CMS injected with
dangerouslySetInnerHTMLinto a<p>. React cannot warn about it, but the browser splits it the same way.
See invalid HTML nesting for the other rules the parser enforces.
How to fix it
-
Find the paragraph. In React 18, the
validateDOMNesting(...): <div> cannot appear as a descendant of <p>warning names no file in the error itself, so read its component stack from the top: the first component listed renders the<div>, and the first one afterat prenders the paragraph. -
Change one of the two elements. Use a
<span>(styled as a block if needed) inside, or a<div>outside. Before:components/card.tsx import type { ReactNode } from "react"; export function Card({ title, children, }: { title: string; children: ReactNode; }) { return ( <p className="card"> <strong>{title}</strong> <div className="card-body">{children}</div> </p> ); }After:
components/card.tsx import type { ReactNode } from "react"; export function Card({ title, children, }: { title: string; children: ReactNode; }) { return ( <div className="card"> <p> <strong>{title}</strong> </p> <div className="card-body">{children}</div> </div> ); } -
In MUI, change what
Typographyrenders. Itscomponentprop picks the element:components/total.tsx import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; export function Total() { return ( <Typography component="div"> <Box sx={{ display: "flex", gap: 1 }}> <span>Total</span> <strong>$42</strong> </Box> </Typography> ); } -
In Markdown renderers, render images with inline elements only, or render paragraphs as
<div>.
Find every instance
A linter only sees nesting inside one component. hydration-proof reads the
real server HTML, so it also finds <p><Card /></p> across components and
HTML from a CMS. It reports each case as HP3001, with
the line in the server HTML:
npx hydration-proof testno-invalid-interactive-nesting
reports every element that closes an open <p>, including in Server
Components, while you type.