Skip to content

[utils] Empty string className causes resolveProps to drop default className #48287

@rosso-off

Description

@rosso-off

Search keywords

mergeClassNameAndStyle className resolveProps

Latest version

  • I have tested the latest version

Steps to reproduce

Steps:

  1. Open this link to live example: reproduce link
  2. Look at the colored TextField — it has the default class.
  3. Look at the transparent TextField — it doesn’t have the default class, but it should.

Current behavior

Description

When mergeClassNameAndStyle: true is configured in the theme's components, the default className from defaultProps is silently dropped if a component receives an empty string "" as className.

This commonly happens when className is computed via clsx() and all inputs are falsy — clsx(undefined, false, null) returns "", not undefined.

Steps to reproduce

const theme = createTheme({
  components: {
    mergeClassNameAndStyle: true,
    MuiTextField: {
      defaultProps: { className: 'my-default-class' }
    }
  }
});

// ✅ Case 1: no className → default applied
<TextField />
// → className="my-default-class"

// ✅ Case 2: non-empty className → merge works
<TextField className="custom" />
// → className="my-default-class custom"

// ❌ Case 3: empty string → default LOST
<TextField className="" />
// → className=""

// ❌ Case 4: clsx with all falsy → same as Case 3
<TextField className={clsx(maybeUndefined, maybeFalse)} />
// clsx() returns "" → default class is lost

Actual behavior
The default className is silently dropped. The component renders with className="".

Root cause
In https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/resolveProps/resolveProps.ts, the className resolution has a gap between two conditions:

// Line 1: merge path — requires props.className to be TRUTHY
if (propName === 'className' && mergeClassNameAndStyle && props.className) {
    output.className = clsx(defaultProps?.className, props?.className);
}
// Line 2: fallback path — requires output[propName] to be UNDEFINED
else if (output[propName] === undefined) {
    output[propName] = defaultProps[propName];
}

When props.className === "":
Merge path is skipped — "" is falsy, so && props.className !== undefined, so condition is false
Result: output.className stays as "", and the default class is never applied.

Expected behavior

When className is an empty string, the default className from theme defaultProps should be applied (same as when className is undefined).

Metadata

Metadata

Assignees

No one assigned

    Labels

    customization: themeHigher level theming customizability.type: bugIt doesn't behave as expected.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions