Hydration Proof

Search documentation

Find a page or section

In HTML, div cannot be a descendant of p

Why a block element inside a paragraph breaks hydration.

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 until Card renders 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 dangerouslySetInnerHTML into 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

  1. 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 after at p renders the paragraph.

  2. 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>
      );
    }
  3. In MUI, change what Typography renders. Its component prop 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>
      );
    }
  4. 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 test

no-invalid-interactive-nesting reports every element that closes an open <p>, including in Server Components, while you type.