A styled-components hydration error happens when the server and the browser
generate different class names for the same component: sc-abc123 in the
server HTML, sc-xyz789 during hydration. Missing server setup or styled
components created in a different order cause it. Turn on the compiler plugin,
add a style registry, and create styled components at module level.
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.In React 19 production builds there is no error at all: React keeps the
server's classes, so the element shows the wrong styles or none. MUI and other
Emotion-based libraries show the same symptom with css- or Mui* class
names.
hydration-proof reports it as HP1004 with the cause
CSS-in-JS class names differ. It recognizes generated class names
(sc-, css-, emotion-, jsx-, makeStyles-) in the differing tokens; in
the package's test suite it names the cause with 90% confidence.
Why a styled-components hydration error happens
A CSS-in-JS library turns each styled component into a generated class name. The name has to come out the same on the server and in the browser, and three things break that:
- 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.
The second case, in its smallest form:
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
Next.js compiles styled-components with SWC. The plugin gives every styled component a stable id and display name instead of a counter:
module.exports = {
compiler: {
styledComponents: true,
},
};Add a style registry for the App Router
The registry collects the styles generated during a server render and inserts
them into the HTML with useServerInsertedHTML. This is the setup from the
Next.js CSS-in-JS guide 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 typeof window branch in the registry is safe: it only changes which
wrapper collects styles, not what the page renders. The Pages Router uses
ServerStyleSheet in a custom _document instead; the
with-styled-components example
shows both.
Create styled components at module level
Define every styled component once, at the top level of a module, and choose between them 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
Material UI styles with Emotion, which needs its own cache setup for server
rendering. MUI ships it in @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>
);
}The import path names the Next.js major version (v15-appRouter,
v16-appRouter); use the one that matches yours. The Pages Router uses
AppCacheProvider and documentGetInitialProps from the matching
v15-pagesRouter entry. See
MUI's Next.js integration guide.
For other libraries, check the list of CSS-in-JS libraries with App Router support in the Next.js guide above. Each needs its own registry or provider.
Catch it with ESLint
There is no lint rule for this cause: whether class names match depends on
the build setup, not on one line of code. hydration-proof test finds it in
the running app.
Catch it in CI
hydration-proof test reports each class difference as HP1004, including the
ones React 19 production builds never report. Run the production build in CI,
where users see the wrong styles, and the development build for exact source
lines:
npx hydration-proof test --mode bothNo probe factor changes generated class names, so the cause comes from the shape of the names and the styled component near the element.