# Fix the styled-components hydration error

> A styled-components hydration error means the server and browser generated different class names. Turn on the compiler and add a style registry.

Source: https://hydration.jscrate.dev/docs/causes/css-in-js
Last updated: 2026-09-18

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

```text
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](https://hydration.jscrate.dev/docs/issues/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:

```tsx title="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:

```js title="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](https://nextjs.org/docs/app/guides/css-in-js) for
styled-components 6:

```tsx title="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>
  );
}
```

```tsx title="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](https://github.com/vercel/next.js/tree/canary/examples/with-styled-components)
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:

```tsx title="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`:

```bash
npm install @mui/material-nextjs @emotion/cache
```

```tsx title="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](https://mui.com/material-ui/integrations/nextjs/).

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:

```bash
npx hydration-proof test --mode both
```

No [probe](https://hydration.jscrate.dev/docs/probes) factor changes generated class names, so the cause
comes from the shape of the names and the styled component near the element.

## Related

- [Prop className did not match](https://hydration.jscrate.dev/docs/errors/prop-classname-did-not-match)
- [HP1004: class name differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1004)
- [Hydration errors in Next.js](https://hydration.jscrate.dev/docs/frameworks/nextjs)
- [Dark mode and theme classes](https://hydration.jscrate.dev/docs/causes/theme)
- [All causes of hydration errors](https://hydration.jscrate.dev/docs/causes)
