Prop className did not match is a React 18 development warning: an element's
class in the server HTML differs from the className React computed in the
browser. It usually comes from a CSS-in-JS library that generated class names
in a different order, or from a theme or screen size read during render. React
keeps the server's class.
The error
React 18 prints the prop name and both values, then a component stack:
Warning: Prop `className` did not match. Server: "sc-abc" Client: "sc-xyz"The same warning exists for every prop. These are the ones you see most:
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>"Production builds do not check props, so there is no minified code. In React 19 the warning is replaced by a diff line in "A tree hydrated but some attributes… didn't match".
What prop className did not match means
React 18 hydrates an element by comparing each prop of your first client render with the attribute in the server HTML. When they differ, it warns and moves on. It does not update the attribute, so the element keeps the server's class, and the styles written for the client's class name do not apply. The component looks unstyled or wrongly styled until it re-renders with a different class.
React 18 warns only once per page load, for the first difference it finds. Fixing one can reveal the next.
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
-
Set up your CSS-in-JS library for server rendering. For styled-components in Next.js, turn on the compiler support, then add the style registry described in CSS-in-JS:
next.config.ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { compiler: { styledComponents: true, }, }; export default nextConfig; -
Define styled components once, at module level. Before, a new component is created 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>; }After, the component is created once and the value is passed 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>; } -
Generate ids with
useId(). For react-select, pass a stableinstanceId:components/country-select.tsx "use client"; import { useId } from "react"; import Select from "react-select"; export function CountrySelect() { return <Select instanceId={useId()} options={[]} />; } -
Move theme and screen size classes out of render. Read them on the server (a cookie) or after hydration, or use CSS media queries.
Find every instance
hydration-proof compares each attribute with the props React rendered and reports class differences as HP1004, other attributes as HP1002, inline styles as HP1003 and injected HTML as HP1013. For elements a library created, it points at the component that used the library:
npx hydration-proof testNo linter can see CSS-in-JS class generation, but the
ESLint plugin catches the other sources:
no-unstable-id,
no-random-in-render,
no-match-media-in-render and
no-browser-global-in-render.