A styled-components hydration error means the same component gets different class names on the server and in the browser. Check the compiler setting, the App Router style registry, and where you create styled components. Both renders need the same classes and collected styles.
Symptoms
Warning: Prop `className` did not match. Server: "sc-bdVaJa kLmNop" Client: "sc-bwzfXH dEfGhI"
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.React 19 may leave the server class in place without a production warning.
Emotion-based libraries, including MUI, can show a similar problem with
css- or Mui* classes.
hydration-proof reports a changed class as HP1004. Generated class-name prefixes help it suggest CSS-in-JS as the cause.
Why a styled-components hydration error happens
The class name must be stable across both renders. Three setup problems can break that agreement:
- No server rendering setup. Without the compiler plugin, component ids depend on the order in which styled components are created. Anything that changes that order on one side changes every name after it.
- Components created conditionally. A
styled()call inside a component, or behind atypeof windowcheck, runs a different number of times on each side. - Styles never collected on the server. Without a registry, the server HTML has class names but no matching styles, and the library generates the styles again in the browser.
For example, a conditional styled() call can change creation order:
import styled from "styled-components";
// Before: an extra styled component on the server shifts every id after it
if (typeof window === "undefined") styled.h2``;
const Title = styled.h2`
color: tomato;
`;How to fix it
Turn on the styled-components compiler
Enable the Next.js styled-components compiler option for stable component ids:
module.exports = {
compiler: {
styledComponents: true,
},
};Add a style registry for the App Router
In the App Router, collect styles during the server render and insert them into the HTML. The Next.js CSS-in-JS guide shows this registry for styled-components 6:
"use client";
import React, { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode;
}) {
// Only create the stylesheet once, with lazy initial state.
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== "undefined") return <>{children}</>;
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
}import StyledComponentsRegistry from "./lib/registry";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
);
}The registry's typeof window branch changes only its style-collection
wrapper, not the page's output. In the Pages Router, use ServerStyleSheet
in a custom _document. See the
Next.js styled-components example.
Create styled components at module level
Define styled components at module level, then vary their appearance with props:
import styled from "styled-components";
const Title = styled.h2<{ $muted?: boolean }>`
color: ${(props) => (props.$muted ? "gray" : "tomato")};
`;
export function Heading({ muted }: { muted?: boolean }) {
return <Title $muted={muted}>Styled title</Title>;
}Fix the MUI Next.js hydration error
For an MUI Next.js hydration error, check Emotion's server cache setup.
MUI provides @mui/material-nextjs:
npm install @mui/material-nextjs @emotion/cacheimport { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
</body>
</html>
);
}Choose the import path for your Next.js major version. The Pages Router uses a different provider. Follow MUI's integration guide for your router and version.
Other CSS-in-JS libraries may need their own registry or provider.
Catch it with ESLint
A single-file lint rule cannot verify the server style setup. Check the
running app with hydration-proof test.
Catch it in CI
Use both modes to see development source locations and production class differences:
npx hydration-proof test --mode bothA probe does not change CSS-in-JS class generation. Inspect the class names and the styled component near the finding.