Skip to content

[system] sx typography shorthand with responsive values mutates theme.typography objects #48265

@tomups

Description

@tomups

Steps to reproduce

Live reproduction: https://github.com/tomups/mui-sx-theme-mutation-repro

git clone https://github.com/tomups/mui-sx-theme-mutation-repro
cd mui-sx-theme-mutation-repro
npm install
npm run dev

Steps:

  1. Open the app in a browser (resize to md+ breakpoint, ≥900px)
  2. Observe the theme.typography.h4 dump at the bottom — it already contains width and marginTop properties that don't belong to it
  3. The plain <Typography variant="h4"> above it picks up those stray properties

The triggering component is:

<Box sx={{ typography: { md: 'h4' }, width: { md: '80%' }, mt: { md: 4 } }}>

Current behavior

After the component renders, theme.typography.h4 is permanently mutated to include CSS properties from sibling sx keys that target the same breakpoint. For the example above, theme.typography.h4 ends up containing { ..., width: '80%', marginTop: '32px' }.

The corruption persists across renders and affects every component that uses variant="h4".

Expected behavior

theme.typography.h4 should not be modified. The sx prop should produce the correct CSS output without mutating the theme.

Context

We hit this upgrading a production app from MUI v7 to v9. Components that use sx={{ typography: { md: 'h4' }, ... }} alongside other responsive properties silently corrupt the theme, causing unrelated components to inherit stray CSS. The symptoms are confusing because they depend on render order and accumulate over time.

The root cause is in setThemeValue in @mui/system/styleFunctionSx/styleFunctionSx.js:

const themeMapping = getPath(theme, themeKey);
iterateBreakpoints(css, theme, value, (mediaKey, valueFinal) => {
  const finalValue = getStyleValue2(themeMapping, transform, valueFinal, prop);
  if (cssProperty === false) {
    if (mediaKey) {
      css[mediaKey] = finalValue;    // assigns theme object by reference
    } else {
      merge(css, finalValue);        // non-responsive path correctly uses merge
    }
  }
});

getStyleValue2 resolves 'h4' to theme.typography.h4 by reference via getPath. The responsive branch then does css[mediaKey] = finalValue, which replaces the breakpoint's CSS accumulator with a direct reference to the live theme object. After that, any other responsive sx key targeting the same breakpoint writes into the theme:

// Processing width: { md: '80%' } for the same breakpoint:
css[mediaKey][cssProperty] = finalValue;
// css[mediaKey] IS theme.typography.h4, so this mutates the theme

The non-responsive path uses merge(css, finalValue) which copies properties without creating a reference. The responsive path should do the same.

This was introduced by #44254.

Your environment

npx @mui/envinfo
  System:
    OS: Linux 6.6 Ubuntu 24.04.4 LTS
  Binaries:
    Node: 24.10.0
    npm: 11.6.1
  Browsers:
    Chrome: 146.0.7680.75
  npmPackages:
    @emotion/react: ^11.14.0 => 11.14.0
    @emotion/styled: ^11.14.1 => 11.14.1
    @mui/material: ^9.0.0 => 9.0.0
    @mui/system: 9.0.0
    @types/react: ^19.0.0 => 19.2.14
    react: ^19.0.0 => 19.2.5
    react-dom: ^19.0.0 => 19.2.5
    typescript: ^5.6.0 => 5.9.3

Search keywords: theme mutation typography sx responsive breakpoint corruption styleFunctionSx

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: systemThe system, the design tokens / styling foundations used across components. eg. @mui/system with MUI

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions