Hydration Proof

Search documentation

Find a page or section

Fix the styled-components hydration error

Generated class names must be generated the same way twice.

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:

  1. 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.
  2. Components created conditionally. A styled() call inside a component, or behind a typeof window check, runs a different number of times on each side.
  3. 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:

title.tsx
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:

next.config.js
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:

lib/registry.tsx
"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>
  );
}
app/layout.tsx
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:

title.tsx
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/cache
app/layout.tsx
import { 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 both

No probe factor changes generated class names, so the cause comes from the shape of the names and the styled component near the element.