Hydration Proof

Search documentation

Find a page or section

Prop className did not match

React 18's warning for attributes, and why the wrong class stays.

Prop className did not match means React 18 found a different class in the server HTML than in the first browser render. Check CSS-in-JS setup, theme state, and screen-size branches. React may leave the server class in place, so the element can look wrong even after hydration.

The error

React 18 shows both class values and a component stack:

Warning: Prop `className` did not match. Server: "sc-abc" Client: "sc-xyz"

Other props can differ too:

Warning: Prop `className` did not match. Server: "MuiBox-root css-1rs9h4" Client: "MuiBox-root css-8kq2d1"
Warning: Prop `id` did not match. Server: "react-select-2-live-region" Client: "react-select-3-live-region"
Warning: Prop `style` did not match. Server: "color:black" Client: "color:white"
Warning: Prop `dangerouslySetInnerHTML` did not match. Server: "<b>Hi</b>" Client: "<b>Hello</b>"

There is no corresponding minified production code for this attribute warning. React 19 shows it in the broader attribute diff: "A tree hydrated but some attributes… didn't match".

What prop className did not match means

React compares the first browser prop with the server attribute. It may warn without updating the DOM class. Styles for the browser's class then do not apply until a later render changes it.

Fix the first difference, then check again for others.

Common causes

  • styled-components without server rendering set up. Class names such as sc-abc come from a component id and a hash of the styles. Without the compiler plugin, the ids depend on the order components were created, which differs between the server and the browser. See CSS-in-JS class names.
  • Emotion or MUI without the cache provider. css- hashes are inserted in a different order on each side. MUI's Next.js integration, @mui/material-nextjs, provides the AppRouterCacheProvider for this.
  • Styled components created inside render. Every render creates a new component with a new id.
  • Theme classes. A dark class chosen from localStorage or matchMedia during render. See theme hydration errors.
  • Screen size. A class chosen from window.innerWidth. See media queries.
  • Ids from counters or Math.random(), including libraries that count instances, such as react-select. See unstable ids.
  • dangerouslySetInnerHTML built from values that differ, such as a date or a user's locale.

How to fix it

  1. Check CSS-in-JS server setup. For styled-components in Next.js, enable compiler support and add the style registry:

    next.config.ts
    import type { NextConfig } from "next";
     
    const nextConfig: NextConfig = {
      compiler: {
        styledComponents: true,
      },
    };
     
    export default nextConfig;
  2. Define styled components at module level. This version creates a new one on every render:

    components/badge.tsx
    "use client";
     
    import styled from "styled-components";
     
    export function Badge({ tone }: { tone: string }) {
      const Pill = styled.span`
        color: ${tone};
      `;
      return <Pill>New</Pill>;
    }

    Move it outside the function and pass the changing value as a prop:

    components/badge.tsx
    "use client";
     
    import styled from "styled-components";
     
    const Pill = styled.span<{ $tone: string }>`
      color: ${(props) => props.$tone};
    `;
     
    export function Badge({ tone }: { tone: string }) {
      return <Pill $tone={tone}>New</Pill>;
    }
  3. Use stable ids. For react-select, pass an instanceId from useId():

    components/country-select.tsx
    "use client";
     
    import { useId } from "react";
    import Select from "react-select";
     
    export function CountrySelect() {
      return <Select instanceId={useId()} options={[]} />;
    }
  4. Avoid browser-only class choices during render. Use a server-readable preference, an effect, or CSS media queries.

Find every instance

hydration-proof reports class differences as HP1004, other attributes as HP1002, styles as HP1003, and injected HTML as HP1013:

npx hydration-proof test

A linter cannot see the final generated class, but the ESLint plugin catches some causes: no-unstable-id, no-random-in-render, no-match-media-in-render and no-browser-global-in-render.