Prop className did not match means React 18 found a different class in the server HTML than in the first browser render. Check CSS-in-JS setup, theme state, and screen-size branches. React may leave the server class in place, so the element can look wrong even after hydration.
The error
React 18 shows both class values and a component stack:
Warning: Prop `className` did not match. Server: "sc-abc" Client: "sc-xyz"Other props can differ too:
Warning: Prop `className` did not match. Server: "MuiBox-root css-1rs9h4" Client: "MuiBox-root css-8kq2d1"
Warning: Prop `id` did not match. Server: "react-select-2-live-region" Client: "react-select-3-live-region"
Warning: Prop `style` did not match. Server: "color:black" Client: "color:white"
Warning: Prop `dangerouslySetInnerHTML` did not match. Server: "<b>Hi</b>" Client: "<b>Hello</b>"There is no corresponding minified production code for this attribute warning. React 19 shows it in the broader attribute diff: "A tree hydrated but some attributes… didn't match".
What prop className did not match means
React compares the first browser prop with the server attribute. It may warn without updating the DOM class. Styles for the browser's class then do not apply until a later render changes it.
Fix the first difference, then check again for others.
Common causes
- styled-components without server rendering set up. Class names such as
sc-abccome from a component id and a hash of the styles. Without the compiler plugin, the ids depend on the order components were created, which differs between the server and the browser. See CSS-in-JS class names. - Emotion or MUI without the cache provider.
css-hashes are inserted in a different order on each side. MUI's Next.js integration,@mui/material-nextjs, provides theAppRouterCacheProviderfor this. - Styled components created inside render. Every render creates a new component with a new id.
- Theme classes. A
darkclass chosen fromlocalStorageormatchMediaduring render. See theme hydration errors. - Screen size. A class chosen from
window.innerWidth. See media queries. - Ids from counters or
Math.random(), including libraries that count instances, such as react-select. See unstable ids. dangerouslySetInnerHTMLbuilt from values that differ, such as a date or a user's locale.
How to fix it
-
Check CSS-in-JS server setup. For styled-components in Next.js, enable compiler support and add the style registry:
next.config.ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { compiler: { styledComponents: true, }, }; export default nextConfig; -
Define styled components at module level. This version creates a new one on every render:
components/badge.tsx "use client"; import styled from "styled-components"; export function Badge({ tone }: { tone: string }) { const Pill = styled.span` color: ${tone}; `; return <Pill>New</Pill>; }Move it outside the function and pass the changing value as a prop:
components/badge.tsx "use client"; import styled from "styled-components"; const Pill = styled.span<{ $tone: string }>` color: ${(props) => props.$tone}; `; export function Badge({ tone }: { tone: string }) { return <Pill $tone={tone}>New</Pill>; } -
Use stable ids. For react-select, pass an
instanceIdfromuseId():components/country-select.tsx "use client"; import { useId } from "react"; import Select from "react-select"; export function CountrySelect() { return <Select instanceId={useId()} options={[]} />; } -
Avoid browser-only class choices during render. Use a server-readable preference, an effect, or CSS media queries.
Find every instance
hydration-proof reports class differences as HP1004, other attributes as HP1002, styles as HP1003, and injected HTML as HP1013:
npx hydration-proof testA linter cannot see the final generated class, but the
ESLint plugin catches some causes:
no-unstable-id,
no-random-in-render,
no-match-media-in-render and
no-browser-global-in-render.