# Common causes of hydration errors

> The common causes of hydration errors in React and Next.js, how hydration-proof picks the cause of each finding, and the ESLint rule that catches each one.

Source: https://hydration.jscrate.dev/docs/causes
Last updated: 2026-09-18

The common causes of hydration errors all break one rule: the first render in
the browser must produce exactly the HTML the server sent. Clocks, random
values, locales, time zones, browser-only APIs, storage, themes, different data
and invalid nesting each break it in their own way. Every cause below has a fix
guide.

## All common causes of hydration errors

hydration-proof attaches one of these cause ids to each finding, and links the
finding to the page for that id:

| Cause id | What it means |
| --- | --- |
| [time](https://hydration.jscrate.dev/docs/causes/time) | Time-dependent value |
| [timezone](https://hydration.jscrate.dev/docs/causes/timezone) | Timezone difference |
| [locale](https://hydration.jscrate.dev/docs/causes/locale) | Locale-dependent formatting |
| [random](https://hydration.jscrate.dev/docs/causes/random) | Random value |
| [browser-api](https://hydration.jscrate.dev/docs/causes/browser-api) | Browser-only API used during render |
| [storage](https://hydration.jscrate.dev/docs/causes/storage) | localStorage or sessionStorage read during render |
| [media-query](https://hydration.jscrate.dev/docs/causes/media-query) | Screen size or media query read during render |
| [theme](https://hydration.jscrate.dev/docs/causes/theme) | Theme preference (dark or light mode) |
| [data](https://hydration.jscrate.dev/docs/causes/data) | Server and client used different data |
| [invalid-html](https://hydration.jscrate.dev/docs/causes/invalid-html) | Invalid HTML nesting |
| [css-in-js](https://hydration.jscrate.dev/docs/causes/css-in-js) | CSS-in-JS class names differ |
| [extension](https://hydration.jscrate.dev/docs/causes/extension) | Browser extension |
| [third-party-script](https://hydration.jscrate.dev/docs/causes/third-party-script) | A script changed the page before hydration |
| [cdn](https://hydration.jscrate.dev/docs/causes/cdn) | HTML rewritten by a CDN or proxy |
| [unstable-id](https://hydration.jscrate.dev/docs/causes/unstable-id) | Generated id differs |
| [form-state](https://hydration.jscrate.dev/docs/causes/form-state) | Server Action form state |
| [suppressed](https://hydration.jscrate.dev/docs/causes/suppressed) | Intentional difference (suppressHydrationWarning) |

## What React lists as the causes

React 19 prints its own short list with every hydration error. Each line maps to
one or more of the pages above:

```text
Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. 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.
```

| React's line                       | Cause pages                                                                                                                                       |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| A server/client branch             | [Browser APIs](https://hydration.jscrate.dev/docs/causes/browser-api), [storage](https://hydration.jscrate.dev/docs/causes/storage), [media queries](https://hydration.jscrate.dev/docs/causes/media-query), [theme](https://hydration.jscrate.dev/docs/causes/theme) |
| `Date.now()` or `Math.random()`    | [Time](https://hydration.jscrate.dev/docs/causes/time), [random values](https://hydration.jscrate.dev/docs/causes/random), [generated ids](https://hydration.jscrate.dev/docs/causes/unstable-id)                                        |
| Date formatting in a user's locale | [Locale](https://hydration.jscrate.dev/docs/causes/locale), [timezone](https://hydration.jscrate.dev/docs/causes/timezone)                                                                                  |
| External changing data             | [Data](https://hydration.jscrate.dev/docs/causes/data), [form state](https://hydration.jscrate.dev/docs/causes/form-state)                                                                                  |
| Invalid HTML tag nesting           | [Invalid HTML](https://hydration.jscrate.dev/docs/causes/invalid-html)                                                                                                         |
| A browser extension                | [Extensions](https://hydration.jscrate.dev/docs/causes/extension), [third-party scripts](https://hydration.jscrate.dev/docs/causes/third-party-script), [CDNs](https://hydration.jscrate.dev/docs/causes/cdn)                            |

The list leaves out [CSS-in-JS class names](https://hydration.jscrate.dev/docs/causes/css-in-js), which
differ when a style library generates them in a different order on each side,
and [intentional differences](https://hydration.jscrate.dev/docs/causes/suppressed) hidden with
`suppressHydrationWarning`.

## How hydration-proof picks a cause

The likely cause is an informed guess built from four kinds of evidence. The
strongest signal wins, and every other signal that points at the same cause
adds a little confidence:

1. **The two values.** Millisecond timestamps, the same number with different
   separators, random decimals and UUIDs, class names that look generated
   (`sc-`, `css-`, `emotion-`), theme classes (`dark`, `light`), React ids and
   whitespace-only differences each point at one cause.
2. **The code near the line.** hydration-proof maps the element to your source
   file and scans the component that rendered it for `Date.now()`,
   `localStorage`, `matchMedia`, `fetch`, `typeof window` and similar calls.
   Code in a neighboring component never explains a finding.
3. **The scenario.** A browser timezone or locale that differs from the
   server's, dark mode, a mobile viewport or pre-filled storage.
4. **The stage.** Markup the browser's parser repaired is invalid nesting. A
   change between parsing and hydration came from a script or an extension. A
   form the server rendered with Server Action state is form state. A
   difference on an element with `suppressHydrationWarning` is intentional.

The report shows the cause with a confidence score, for example
`timezone difference, 95%`, and the reason next to it. Below 35%, no cause is
named. Some causes cannot be read off the values at all: a `typeof window`
branch only shows in the code, so run in development mode for exact source
lines. [How it works](https://hydration.jscrate.dev/docs/how-it-works) covers the six snapshots behind each
comparison.

## Prove the cause with probes

`--probe` turns the guess into proof. hydration-proof loads the page again with
the browser clock and random values fixed, then once for each factor with
exactly one thing changed:

| Factor     | What changes                                                                              |
| ---------- | ----------------------------------------------------------------------------------------- |
| `time`     | The browser clock moves by 3 days, 7 hours, 11 minutes and 13 seconds                     |
| `random`   | A different seed for `Math.random()` and `crypto.getRandomValues()`                       |
| `locale`   | The server's locale, or another one                                                       |
| `timezone` | The server's timezone, or another one                                                     |
| `theme`    | Light and dark swap                                                                       |
| `viewport` | Desktop and mobile swap                                                                   |
| `storage`  | The page loads without the scenario's `localStorage`, `sessionStorage` and `storageState` |

If the client value changes, or the finding disappears, when only one factor
changes, that factor is the proven cause, and the report says `proven` instead
of a percentage. A page that renders differently on an
identical reload depends on server data. The server is never changed, so a
fixed browser clock cannot hide a time-dependent value. See
[probes](https://hydration.jscrate.dev/docs/probes) for the options.

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

## Causes, ESLint rules and issue codes

The [ESLint plugin](https://hydration.jscrate.dev/docs/eslint) catches most causes while you type.
hydration-proof catches all of them in the running app, including the ones no
linter can see. Confidence is what the package's own fixture suite measured for
a production build.

| Cause                                                 | Typical issue codes                                                  | ESLint rules                                                                                                                                                                                                           | Measured confidence |
| ----------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| [time](https://hydration.jscrate.dev/docs/causes/time)                             | [HP1001](https://hydration.jscrate.dev/docs/issues/hp1001), [HP1002](https://hydration.jscrate.dev/docs/issues/hp1002)         | [`no-date-in-render`](https://hydration.jscrate.dev/docs/rules/no-date-in-render)                                                                                                                                                                   | 97%                 |
| [timezone](https://hydration.jscrate.dev/docs/causes/timezone)                     | HP1001                                                               | [`no-timezone-without-explicit-timezone`](https://hydration.jscrate.dev/docs/rules/no-timezone-without-explicit-timezone)                                                                                                                           | 95%                 |
| [locale](https://hydration.jscrate.dev/docs/causes/locale)                         | HP1001                                                               | [`no-locale-without-explicit-locale`](https://hydration.jscrate.dev/docs/rules/no-locale-without-explicit-locale)                                                                                                                                   | 98%                 |
| [random](https://hydration.jscrate.dev/docs/causes/random)                         | HP1001, HP1002                                                       | [`no-random-in-render`](https://hydration.jscrate.dev/docs/rules/no-random-in-render)                                                                                                                                                               | 94%                 |
| [browser-api](https://hydration.jscrate.dev/docs/causes/browser-api)               | HP1001, HP1002, [HP1003](https://hydration.jscrate.dev/docs/issues/hp1003)                        | [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render), [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch), [`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state) | 57–59%              |
| [storage](https://hydration.jscrate.dev/docs/causes/storage)                       | HP1001                                                               | [`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render)                                                                                                                                             | 94%                 |
| [media-query](https://hydration.jscrate.dev/docs/causes/media-query)               | HP1001, [HP1004](https://hydration.jscrate.dev/docs/issues/hp1004)                                | [`no-match-media-in-render`](https://hydration.jscrate.dev/docs/rules/no-match-media-in-render)                                                                                                                                                     | 89%                 |
| [theme](https://hydration.jscrate.dev/docs/causes/theme)                           | HP1004, HP1002                                                       | [`audit-suppress-hydration-warning`](https://hydration.jscrate.dev/docs/rules/audit-suppress-hydration-warning)                                                                                                                                     | 99%                 |
| [data](https://hydration.jscrate.dev/docs/causes/data)                             | HP1001, [HP1007](https://hydration.jscrate.dev/docs/issues/hp1007)–[HP1009](https://hydration.jscrate.dev/docs/issues/hp1009)  | none                                                                                                                                                                                                                   | 68%                 |
| [invalid-html](https://hydration.jscrate.dev/docs/causes/invalid-html)             | [HP3001](https://hydration.jscrate.dev/docs/issues/hp3001), [HP3002](https://hydration.jscrate.dev/docs/issues/hp3002)         | [`no-invalid-interactive-nesting`](https://hydration.jscrate.dev/docs/rules/no-invalid-interactive-nesting)                                                                                                                                         | 97%                 |
| [css-in-js](https://hydration.jscrate.dev/docs/causes/css-in-js)                   | HP1004                                                               | none                                                                                                                                                                                                                   | 90%                 |
| [extension](https://hydration.jscrate.dev/docs/causes/extension)                   | [HP4002](https://hydration.jscrate.dev/docs/issues/hp4002), [HP4001](https://hydration.jscrate.dev/docs/issues/hp4001)         | none                                                                                                                                                                                                                   | not measured        |
| [third-party-script](https://hydration.jscrate.dev/docs/causes/third-party-script) | HP4001                                                               | none                                                                                                                                                                                                                   | 80%                 |
| [cdn](https://hydration.jscrate.dev/docs/causes/cdn)                               | [HP1015](https://hydration.jscrate.dev/docs/issues/hp1015), [HP4003](https://hydration.jscrate.dev/docs/issues/hp4003)         | none                                                                                                                                                                                                                   | 88%                 |
| [unstable-id](https://hydration.jscrate.dev/docs/causes/unstable-id)               | HP1002, [HP3003](https://hydration.jscrate.dev/docs/issues/hp3003), [HP3004](https://hydration.jscrate.dev/docs/issues/hp3004) | [`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/no-unstable-id), [`no-global-render-counter`](https://hydration.jscrate.dev/docs/rules/no-global-render-counter)                                                                                                     | not measured        |
| [form-state](https://hydration.jscrate.dev/docs/causes/form-state)                 | [HP1012](https://hydration.jscrate.dev/docs/issues/hp1012)                                        | none                                                                                                                                                                                                                   | not measured        |
| [suppressed](https://hydration.jscrate.dev/docs/causes/suppressed)                 | [HP6001](https://hydration.jscrate.dev/docs/issues/hp6001)–[HP6003](https://hydration.jscrate.dev/docs/issues/hp6003)          | [`audit-suppress-hydration-warning`](https://hydration.jscrate.dev/docs/rules/audit-suppress-hydration-warning)                                                                                                                                     | not measured        |

`browser-api` scores lower because the values alone never show it: the cause
comes from the code, which is exact in development builds and depends on
source maps in production.

## Related

- [React hydration errors explained](https://hydration.jscrate.dev/docs/guides/react-hydration-error)
- [Every React hydration error message](https://hydration.jscrate.dev/docs/errors), decoded
- [All hydration-proof issue codes](https://hydration.jscrate.dev/docs/issues)
- [The 15 ESLint rules](https://hydration.jscrate.dev/docs/rules)
- [Proving a cause with probes](https://hydration.jscrate.dev/docs/probes)
