# A tree hydrated but some attributes of the server rendered HTML didn't match

> A tree hydrated but some attributes of the server rendered HTML didn't match: React 19 kept the page but not the attribute. Why, and how to fix it for good.

Source: https://hydration.jscrate.dev/docs/errors/tree-hydrated-but-attributes-didnt-match
Last updated: 2026-09-18

"A tree hydrated but some attributes of the server rendered HTML didn't match
the client properties" is a React 19 development warning. Hydration worked,
but an attribute such as `className`, `style`, `href` or `id` differs, and
React keeps the server's value. Users see the wrong attribute until that prop
changes. Make the attribute identical in both renders.

## The error

React 19 logs it in development, with a diff of every attribute that differs:

```text
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

  <Sidebar>
    <aside
+     className="sidebar wide"
-     className="sidebar"
    >
```

There is no production form and no minified code: production builds of
React 19 do not compare attributes at all.

In React 18 the same problem produced one warning per attribute, such as
[`Prop className did not match`](https://hydration.jscrate.dev/docs/errors/prop-classname-did-not-match)
and [`Extra attributes from the server`](https://hydration.jscrate.dev/docs/errors/extra-attributes-from-the-server).

## Why a tree hydrated but some attributes of the server rendered HTML didn't match

For attributes, React does not throw the page away as it does for text and
elements. It attaches to the server's DOM and leaves the attribute as the
server wrote it: that is what "this won't be patched up" means.

Later renders do not repair it either. React compares each new render with its
own previous props, not with the DOM, so the DOM keeps the server's value
until the prop itself changes. A `className` computed from the screen width
stays wrong for as long as the width stays the same.

Read the diff like this:

- `+` and `-` on the same attribute: the value differs.
- A `-` line alone: the attribute is only in the server HTML. Often a script
  or a browser extension added it before React loaded.
- A `+` line alone: the browser render has an attribute the server did not
  send.

> **Production is silent**
>
> React 19 checks attributes only in development. The same mismatch in
> production logs nothing, and your users keep the wrong value. Test the
> production build with a tool that compares attributes itself (see below).

## Common causes

| Attribute                              | Usual source                                           | Fix guide                                                |
| -------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- |
| `class` or `style` on `<html>`         | A theme script sets dark mode before React loads       | [Theme](https://hydration.jscrate.dev/docs/causes/theme)                              |
| `className` on components              | CSS-in-JS class names generated in a different order   | [CSS-in-JS](https://hydration.jscrate.dev/docs/causes/css-in-js)                      |
| `className` from the screen size       | `window.innerWidth` or `matchMedia` read during render | [Media queries](https://hydration.jscrate.dev/docs/causes/media-query)                |
| `id`, `htmlFor`, `aria-*`              | Ids from `Math.random()` or a counter                  | [Unstable ids](https://hydration.jscrate.dev/docs/causes/unstable-id)                 |
| `datetime`, `title`, `aria-label`      | A formatted time or date                               | [Time](https://hydration.jscrate.dev/docs/causes/time), [locale](https://hydration.jscrate.dev/docs/causes/locale) |
| Attributes on `<body>` nobody rendered | A browser extension                                    | [Browser extensions](https://hydration.jscrate.dev/docs/causes/extension)             |

## How to fix it

1. **Read the attribute name and both values** in the diff, and the component
   above it.
2. **Compute the attribute from data both sides have.** Before, the class
   depends on the window, which the server does not have:

   ```tsx title="components/sidebar.tsx"
   "use client";

   export function Sidebar() {
     const wide = typeof window !== "undefined" && window.innerWidth > 1024;
     return <aside className={wide ? "sidebar wide" : "sidebar"} />;
   }
   ```

   After, both renders use the default and an effect updates it. Because this
   is a state change, React applies it to the DOM:

   ```tsx title="components/sidebar.tsx"
   "use client";

   import { useEffect, useState } from "react";

   export function Sidebar() {
     const [wide, setWide] = useState(false);

     useEffect(() => {
       const update = () => setWide(window.innerWidth > 1024);
       update();
       window.addEventListener("resize", update);
       return () => window.removeEventListener("resize", update);
     }, []);

     return <aside className={wide ? "sidebar wide" : "sidebar"} />;
   }
   ```

   For layout alone, a CSS media query avoids the second render entirely.

3. **For ids, use `useId()`.** It produces the same id on the server and in
   the browser.
4. **For attributes a pre-hydration script sets on purpose** (a theme class on
   `<html>`), add `suppressHydrationWarning` to that one element. It covers the
   element's own attributes only. See
   [suppressHydrationWarning](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning).

## Find every instance

hydration-proof compares every attribute in the page with the props React
rendered, in development and in production, where React itself stays silent:

```bash
npx hydration-proof test --mode both
```

Differences are reported as [HP1002](https://hydration.jscrate.dev/docs/issues/hp1002) (an attribute),
[HP1003](https://hydration.jscrate.dev/docs/issues/hp1003) (inline style),
[HP1004](https://hydration.jscrate.dev/docs/issues/hp1004) (class), [HP1005](https://hydration.jscrate.dev/docs/issues/hp1005) (only
in the server HTML) and [HP1006](https://hydration.jscrate.dev/docs/issues/hp1006) (missing from it).

In the editor:
[`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render),
[`no-match-media-in-render`](https://hydration.jscrate.dev/docs/rules/no-match-media-in-render),
[`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/no-unstable-id) and
[`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state)
report the code that produces these attributes.

## Related

- [Hydration failed: the React 19 error for text and elements](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client)
- [Fix the next-themes hydration warning](https://hydration.jscrate.dev/docs/causes/theme)
- [CSS-in-JS class name mismatches](https://hydration.jscrate.dev/docs/causes/css-in-js)
- [HP1002: attribute differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1002)
- [Errors that only show up in production](https://hydration.jscrate.dev/docs/guides/hydration-error-only-in-production)
