# Expected server HTML to contain a matching element

> Expected server HTML to contain a matching div in div: React 18 rendered an element in the browser that the server HTML lacks. Each variant and its fix.

Source: https://hydration.jscrate.dev/docs/errors/expected-server-html-to-contain-a-matching
Last updated: 2026-09-18

Expected server HTML to contain a matching `<div>` in `<div>` is a React 18
development warning: the first browser render has an element that the server's
HTML does not have. "Did not expect server HTML to contain"
is the reverse. Something renders on one side only, or the browser moved
invalid markup. Render the same elements on both sides.

## The error

The two forms, as React 18 prints them:

```text
Warning: Expected server HTML to contain a matching <div> in <div>.
Warning: Did not expect server HTML to contain a <div> in <div>.
```

The tags change with your markup. These are the variants people see most:

```text
Warning: Expected server HTML to contain a matching <div> in <body>.
Warning: Expected server HTML to contain a matching <tr> in <table>.
Warning: Expected server HTML to contain a matching <table> in <div>.
Warning: Expected server HTML to contain a matching <meta> in <head>.
Warning: Expected server HTML to contain a matching text node for "Loading…" in <div>.
Warning: Did not expect server HTML to contain the text node "Loading…" in <div>.
```

Right after the warning, React 18 throws
["Hydration failed because the initial UI does not match what was rendered on the server"](https://hydration.jscrate.dev/docs/errors/hydration-failed-initial-ui-does-not-match),
or #418 in production. React 19 shows the same thing as a `+` line (only in
the browser) or a `-` line (only on the server) in the diff of
[its hydration error](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client).

## What expected server HTML to contain a matching element means

Read the warning as "child in parent":

| Warning                                                 | The first tag is           | Meaning                                                        |
| ------------------------------------------------------- | -------------------------- | -------------------------------------------------------------- |
| `Expected server HTML to contain a matching <X> in <Y>` | In the browser render only | The server did not render `<X>` there, or the browser moved it |
| `Did not expect server HTML to contain a <X> in <Y>`    | In the server HTML only    | The browser render has nothing there, or something else        |

The component stack printed under the warning names the component that
renders the parent. React 18 logs only the first difference per page, then
renders the page again in the browser.

## Common causes

### An element rendered on one side only

A `typeof window` check, a value from `localStorage`, a "mounted" flag read
during render or a feature flag the browser reads again: the server renders
nothing (or a placeholder) and the browser renders the element. See
[browser-only APIs](https://hydration.jscrate.dev/docs/causes/browser-api) and
[storage](https://hydration.jscrate.dev/docs/causes/storage).

### A table without `<tbody>`

The `<tr>` in `<table>` variant is almost always this. The HTML parser adds a
`<tbody>` around rows it finds directly in a `<table>`, so React finds a
`<tbody>` where it expected your `<tr>`. See
[validateDOMNesting](https://hydration.jscrate.dev/docs/errors/validatedomnesting).

### Invalid nesting the browser repaired

A `<div>`, `<table>` or list inside a `<p>` is moved out of the `<p>` while
the page is parsed. React then expects the element inside the paragraph and
does not find it. See
[div cannot be a descendant of p](https://hydration.jscrate.dev/docs/errors/div-cannot-be-a-descendant-of-p).

### Something else changed the page first

For `<div>` in `<body>`, a browser extension or a third-party script (chat
widgets, consent banners) often inserted or removed elements before React
loaded. For `<meta>` in `<head>`, check tags rendered from browser-only values
and scripts that add their own tags. See
[browser extensions](https://hydration.jscrate.dev/docs/causes/extension) and
[third-party scripts](https://hydration.jscrate.dev/docs/causes/third-party-script).

## How to fix it

1. **Match the tags to your code.** The warning names the child and the
   parent. The component stack names the component.
2. **Wrap table rows in `<tbody>`.** Before:

   ```tsx title="components/prices.tsx"
   type Row = { id: string; name: string; price: string };

   export function Prices({ rows }: { rows: Row[] }) {
     return (
       <table>
         {rows.map((row) => (
           <tr key={row.id}>
             <td>{row.name}</td>
             <td>{row.price}</td>
           </tr>
         ))}
       </table>
     );
   }
   ```

   After:

   ```tsx title="components/prices.tsx"
   type Row = { id: string; name: string; price: string };

   export function Prices({ rows }: { rows: Row[] }) {
     return (
       <table>
         <tbody>
           {rows.map((row) => (
             <tr key={row.id}>
               <td>{row.name}</td>
               <td>{row.price}</td>
             </tr>
           ))}
         </tbody>
       </table>
     );
   }
   ```

3. **Render browser-only elements after hydration.** Before, the banner
   exists only in the browser render:

   ```tsx title="components/cookie-banner.tsx"
   "use client";

   export function CookieBanner() {
     if (typeof window === "undefined") return null;
     if (localStorage.getItem("consent")) return null;
     return <div className="banner">We use cookies.</div>;
   }
   ```

   After, both renders start with nothing, and an effect shows it:

   ```tsx title="components/cookie-banner.tsx"
   "use client";

   import { useEffect, useState } from "react";

   export function CookieBanner() {
     const [show, setShow] = useState(false);

     useEffect(() => {
       setShow(!localStorage.getItem("consent"));
     }, []);

     if (!show) return null;
     return <div className="banner">We use cookies.</div>;
   }
   ```

4. **Test in a clean browser profile** when the parent is `<body>` or
   `<head>`. If the warning disappears, an extension caused it.

## Find every instance

hydration-proof reports an element only the browser rendered as
[HP1009](https://hydration.jscrate.dev/docs/issues/hp1009), one only in the server HTML as
[HP1008](https://hydration.jscrate.dev/docs/issues/hp1008) and a different element in the same place as
[HP1007](https://hydration.jscrate.dev/docs/issues/hp1007). A missing `<tbody>` and other repaired markup
is [HP3001](https://hydration.jscrate.dev/docs/issues/hp3001), and `<head>` differences are
[HP1014](https://hydration.jscrate.dev/docs/issues/hp1014):

```bash
npx hydration-proof test
```

[`no-invalid-interactive-nesting`](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)
reports `<tr>` directly in `<table>` and block elements in `<p>`, and
[`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) and
[`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render)
report the one-sided renders.

## Related

- [Hydration failed: initial UI does not match](https://hydration.jscrate.dev/docs/errors/hydration-failed-initial-ui-does-not-match)
- [Invalid HTML nesting and hydration](https://hydration.jscrate.dev/docs/causes/invalid-html)
- [Render a component only in the browser](https://hydration.jscrate.dev/docs/guides/client-only-component)
- [HP1009: element missing from the server HTML](https://hydration.jscrate.dev/docs/issues/hp1009)
- [All hydration error messages](https://hydration.jscrate.dev/docs/errors)
