The common causes of hydration errors have one thing in common: the first browser render does not match the server HTML. Find the changed value or element, then open the matching cause guide below for a fix.
All common causes of hydration errors
hydration-proof links each finding to a likely cause guide:
| Cause id | What it means |
|---|---|
| time | Time-dependent value |
| timezone | Timezone difference |
| locale | Locale-dependent formatting |
| random | Random value |
| browser-api | Browser-only API used during render |
| storage | localStorage or sessionStorage read during render |
| media-query | Screen size or media query read during render |
| theme | Theme preference (dark or light mode) |
| data | Server and client used different data |
| invalid-html | Invalid HTML nesting |
| css-in-js | CSS-in-JS class names differ |
| extension | Browser extension |
| third-party-script | A script changed the page before hydration |
| cdn | HTML rewritten by a CDN or proxy |
| unstable-id | Generated id differs |
| form-state | Server Action form state |
| suppressed | Intentional difference (suppressHydrationWarning) |
What React lists as the causes
React 19 includes a list of possible causes in its error message:
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, storage, media queries, theme |
Date.now() or Math.random() | Time, random values, generated ids |
| Date formatting in a user's locale | Locale, timezone |
| External changing data | Data, form state |
| Invalid HTML tag nesting | Invalid HTML |
| A browser extension | Extensions, third-party scripts, CDNs |
React's list is not exhaustive. CSS-in-JS class names and suppressed differences can also matter.
How hydration-proof picks a cause
The report suggests a cause using four kinds of evidence:
- 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. - 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 windowand similar calls. Code in a neighboring component never explains a finding. - The scenario. A browser timezone or locale that differs from the server's, dark mode, a mobile viewport or pre-filled storage.
- 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
suppressHydrationWarningis intentional.
The report shows its confidence and the supporting reason. When confidence
is too low, it does not name a cause. A browser branch such as typeof window
is easier to identify in a development build with source locations. See
how it works.
Prove the cause with probes
Use --probe to test a suggested cause. It reloads the page while changing
one browser factor at a time:
| 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 a single change moves or removes the finding, the report marks that factor
as proven. If identical reloads differ, inspect server or API data instead.
See probes for the details.
npx hydration-proof test --probeCauses, ESLint rules and issue codes
The ESLint plugin catches many risky patterns in source code. The browser check finds differences that a linter cannot see. Each cause guide links to its relevant rules. The confidence values below come from the package's production-build fixtures; they are not a promise for every app.
| Cause | Common findings | Fixture confidence |
|---|---|---|
| Time | HP1001, HP1002 | 97% |
| Timezone | HP1001 | 95% |
| Locale | HP1001 | 98% |
| Random values | HP1001, HP1002 | 94% |
| Browser APIs | HP1001, HP1002, HP1003 | 57–59% |
| Storage | HP1001 | 94% |
| Media queries | HP1001, HP1004 | 89% |
| Theme | HP1004, HP1002 | 99% |
| Changing data | HP1001, HP1007–HP1009 | 68% |
| Invalid HTML | HP3001, HP3002 | 97% |
| CSS-in-JS | HP1004 | 90% |
| Extensions | HP4002, HP4001 | Not measured |
| Third-party scripts | HP4001 | 80% |
| CDN changes | HP1015, HP4003 | 88% |
| Generated ids | HP1002, HP3003, HP3004 | Not measured |
| Form state | HP1012 | Not measured |
| Suppressed differences | HP6001–HP6003 | 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.