A timezone hydration mismatch happens when the server and browser format the
same date in different zones. A server in UTC might show "5:00 AM" while a
visitor in Karachi sees "10:00 AM". Pass one explicit timeZone to both
renders, or format in the visitor's zone after hydration.
Symptoms
React reports a text mismatch when the two zones produce different output:
Hydration failed because the server rendered text didn't match the client.
Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "Signed in at 5:00 AM" Client: "Signed in at 10:00 AM"
Minified React error #418; visit https://react.dev/errors/418hydration-proof reports changed text as HP1001 and shows both values:
HP1001 Text differs between server and client · app/dashboard/LastLogin.tsx:14
server "Signed in at 5:00 AM" → client "Signed in at 10:00 AM"
Possible cause: Timezone difference. Fix: Set the same timeZone on both sides.Some dates differ only near midnight, so the bug may affect only certain visitors or times of day.
Why a timezone hydration mismatch happens
A Date represents one moment. Formatting that moment uses a timezone.
Without an explicit zone, each runtime uses its own default. Mixing local
dates in the browser timezone with UTC dates on the server is a common case.
These read the runtime's zone:
toLocaleDateString(),toLocaleTimeString(), andtoLocaleString()on a datenew Intl.DateTimeFormat()without atimeZoneoptiongetHours(),getMinutes(),getDate(),getDay(),getMonth(),getFullYear()andgetTimezoneOffset()toDateString(),toTimeString()andtoString()on a dateIntl.DateTimeFormat().resolvedOptions().timeZone, which returns the zone itself
This applies to stored dates too, not just the current time. If the moment itself changes between renders, start with the time guide.
How to fix it
Use one time zone on both sides
If the server knows the user's timezone from a profile or cookie, pass it to the component:
import { cookies } from "next/headers";
import { LastLogin } from "./last-login";
export default async function Page() {
const timeZone = (await cookies()).get("tz")?.value ?? "UTC";
const user = await getUser();
return <LastLogin at={user.lastLoginAt} timeZone={timeZone} />;
}"use client";
export function LastLogin({ at, timeZone }: { at: number; timeZone: string }) {
const time = new Date(at).toLocaleTimeString("en-US", {
timeZone,
hour: "numeric",
minute: "2-digit",
});
return <p id="last-login">Signed in at {time}</p>;
}You can save the browser's timezone after hydration for later requests:
"use client";
import { useEffect } from "react";
export function TimeZoneCookie() {
useEffect(() => {
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.cookie = `tz=${zone}; path=/; max-age=31536000; samesite=lax`;
}, []);
return null;
}The first visit uses UTC; later requests can use the saved zone.
Intl.DateTimeFormat timeZone: hydration-safe formatting
If one zone is right for everyone, set it in the formatter. Use UTC getters when reading date parts yourself:
// Before: the runtime's zone, different on each side
const localFormat = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
// After: the same zone everywhere
const format = new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
dateStyle: "medium",
timeStyle: "short",
});
export function Posted({ at }: { at: number }) {
const date = new Date(at);
// getUTCDate() instead of getDate(), getUTCMonth() instead of getMonth()
return (
<p>
{format.format(date)} ({date.getUTCDate()}/{date.getUTCMonth() + 1})
</p>
);
}Set the locale too, or the same time may still be formatted differently. See locale formatting.
Format in the visitor's zone after mount
If the server cannot know the zone, show a stable value first and format the local time after hydration:
"use client";
import { useEffect, useState } from "react";
export function LocalTime({ at }: { at: number }) {
const [text, setText] = useState(() =>
new Date(at).toISOString().slice(0, 10)
);
useEffect(() => {
setText(
new Date(at).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
})
);
}, [at]);
return <time dateTime={new Date(at).toISOString()}>{text}</time>;
}The two-pass rendering guide explains the temporary first value.
i18n libraries
For next-intl, set timeZone in the request config so both sides receive the
same value. See the locale guide.
Catch it with ESLint
no-timezone-without-explicit-timezone
flags local formatters and getters in render code. It suggests an explicit
zone or UTC getter where appropriate.
npm install -D eslint-plugin-hydration-proofCatch it in CI
A local run may use the same timezone on both sides. Add other zones to the environment matrix:
import { defineConfig } from "hydration-proof";
export default defineConfig({
matrix: {
timezoneId: ["UTC", "Asia/Karachi", "America/Los_Angeles"],
},
});The report shows which timezone produced the finding. Use --probe to change
only the browser timezone and test the cause. See the
environment matrix and probes.
npx hydration-proof test --probe