# HP1004: Class name differs between server and client

> HP1004 (class mismatch): the class attribute in the server HTML differs from React's className, often a theme or CSS-in-JS. The page keeps the server classes.

Source: https://hydration.jscrate.dev/docs/issues/hp1004
Last updated: 2026-09-18

HP1004 (`class-mismatch`) means the `class` attribute in the server HTML
differs from the `className` React renders on the client. React keeps the
server classes, so the page shows the wrong theme or styles until the element
re-renders. The usual causes are a dark mode check during render and CSS-in-JS
class names generated in a different order.

| | |
| --- | --- |
| Code | `HP1004` |
| Name | `class-mismatch` |
| Default severity | Error |
| Group | DOM mismatches |
| What it means | The class attribute rendered on the server differs from the className React renders on the client. The page keeps the server classes. |

## What HP1004 (`class-mismatch`) means

The element exists on both sides, but the set of class names differs. Classes
are compared as a set, so the same names in another order do not count.

React does not patch attributes during hydration. In production, React 19
reports nothing and leaves the server classes in place: a user who prefers dark
mode keeps the light theme. hydration-proof compares every reused
element's classes with the `className` React renders, so it finds this in
production builds too.

## The React error it matches

Only development builds report it:

```text
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.
Warning: Prop `className` did not match. Server: "theme-light" Client: "theme-dark"
```

The first is React 19, the second React 18. See
[Prop `className` did not match](https://hydration.jscrate.dev/docs/errors/prop-classname-did-not-match) and
[attributes didn't match](https://hydration.jscrate.dev/docs/errors/tree-hydrated-but-attributes-didnt-match).

## Likely causes

- The [theme](https://hydration.jscrate.dev/docs/causes/theme): the server cannot know the color scheme, so
  a class chosen with `prefers-color-scheme` or a stored preference differs.
  The package's `/dark-mode` test page is found this way.
- [CSS-in-JS class names](https://hydration.jscrate.dev/docs/causes/css-in-js): the library generates class
  names from a counter, and a styled component created on one side only (or
  without the library's server setup) shifts it. The `/css-in-js` test page
  creates one styled component only on the server.
- A [media query](https://hydration.jscrate.dev/docs/causes/media-query) or
  [browser storage](https://hydration.jscrate.dev/docs/causes/storage) read during render.

## How to fix it

- Make class names deterministic between server and client.
- For themes, read the preference on the server (for example from a cookie) or
  set the class with a pre-hydration script and `suppressHydrationWarning` on
  that element.
- For CSS-in-JS, set up the library's server style registry so class names are
  generated in the same order.

A theme read from the browser during render:

```tsx title="theme-preview.tsx"
"use client";

export function ThemePreview() {
  const dark =
    typeof window !== "undefined" &&
    window.matchMedia("(prefers-color-scheme: dark)").matches;

  return <div className={dark ? "theme-dark" : "theme-light"}>Preview</div>;
}
```

Store the theme in a cookie and read it on the server, so the first render
already has the right class (Next.js App Router shown):

```tsx title="app/layout.tsx"
import { cookies } from "next/headers";
import type { ReactNode } from "react";

export default async function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  const theme = (await cookies()).get("theme")?.value ?? "light";

  return (
    <html lang="en" className={theme}>
      <body>{children}</body>
    </html>
  );
}
```

Without a cookie (the first visit), set the class on `<html>` with a small
inline script that runs before hydration, and mark `<html>` with
`suppressHydrationWarning`. The difference is then listed as
[HP6001](https://hydration.jscrate.dev/docs/issues/hp6001) (info), not as an error. See the
[theme cause](https://hydration.jscrate.dev/docs/causes/theme) for the details, and the
[CSS-in-JS cause](https://hydration.jscrate.dev/docs/causes/css-in-js) for the style registry.

## Prevent it with ESLint

[`no-match-media-in-render`](https://hydration.jscrate.dev/docs/rules/no-match-media-in-render) reports
`matchMedia` in render code, including the color scheme query, and
[`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render) a
theme read from storage.

## When the difference is intentional

A theme class set by a pre-hydration script is the case
`suppressHydrationWarning` exists for. For a legacy area you cannot fix yet,
add an `ignore.issues` rule with `code: "HP1004"`, a route, a reason and an
expiry date; see [ignoring findings](https://hydration.jscrate.dev/docs/ignoring).

## Example

```text
  ✖ /settings 687ms  1 error
    HP1004 Class name differs between server and client  (theme preference (dark/light mode), 99%)
      #theme  in ThemePreview
      attribute: class
      server: "theme-light"
      client: "theme-dark"
      app/settings/ThemePreview.tsx:9:5
      → The server cannot know the color scheme. Store the theme in a cookie and read it on the server, or set the class with an inline script before hydration and mark that element with suppressHydrationWarning.
```

## Related

- [Fix dark mode hydration mismatches](https://hydration.jscrate.dev/docs/causes/theme)
- [CSS-in-JS class names that differ](https://hydration.jscrate.dev/docs/causes/css-in-js)
- [HP1002: attribute differs](https://hydration.jscrate.dev/docs/issues/hp1002)
- [HP1003: inline style differs](https://hydration.jscrate.dev/docs/issues/hp1003)
- [HP6001: a mismatch hidden by suppressHydrationWarning](https://hydration.jscrate.dev/docs/issues/hp6001)
