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 is a React 18 development warning: an element's class in the server HTML differs from the className React computed in the browser. It usually comes from a CSS-in-JS library that generated class names in a different order, or from a theme or screen size read during render. React keeps the server's class.

The error

React 18 prints the prop name and both values, then a component stack:

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

The same warning exists for every prop. These are the ones you see most:

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>"

Production builds do not check props, so there is no minified code. In React 19 the warning is replaced by a diff line in "A tree hydrated but some attributes… didn't match".

What prop className did not match means

React 18 hydrates an element by comparing each prop of your first client render with the attribute in the server HTML. When they differ, it warns and moves on. It does not update the attribute, so the element keeps the server's class, and the styles written for the client's class name do not apply. The component looks unstyled or wrongly styled until it re-renders with a different class.

React 18 warns only once per page load, for the first difference it finds. Fixing one can reveal the next.

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. Set up your CSS-in-JS library for server rendering. For styled-components in Next.js, turn on the compiler support, then add the style registry described in CSS-in-JS:

    next.config.ts
    import type { NextConfig } from "next";
     
    const nextConfig: NextConfig = {
      compiler: {
        styledComponents: true,
      },
    };
     
    export default nextConfig;
  2. Define styled components once, at module level. Before, a new component is created 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>;
    }

    After, the component is created once and the value is passed 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. Generate ids with useId(). For react-select, pass a stable instanceId:

    components/country-select.tsx
    "use client";
     
    import { useId } from "react";
    import Select from "react-select";
     
    export function CountrySelect() {
      return <Select instanceId={useId()} options={[]} />;
    }
  4. Move theme and screen size classes out of render. Read them on the server (a cookie) or after hydration, or use CSS media queries.

Find every instance

hydration-proof compares each attribute with the props React rendered and reports class differences as HP1004, other attributes as HP1002, inline styles as HP1003 and injected HTML as HP1013. For elements a library created, it points at the component that used the library:

npx hydration-proof test

No linter can see CSS-in-JS class generation, but the ESLint plugin catches the other sources: no-unstable-id, no-random-in-render, no-match-media-in-render and no-browser-global-in-render.