mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.64k stars 32.22k forks source link

[mui-6] CssVarsProvider createTheme palette.mode ignores existing colorSchemes #43996

Open ggascoigne opened 1 week ago

ggascoigne commented 1 week ago

Summary

I would like createTheme to use existing colorScheme definitions when initialized with a mode and simply apply them to the theme, as opposed to generating a default and applying it to the theme. I'd like this to only generate a default is a suitable colorScheme had not already been provided.

Examples

My Theme looks something like this:

{
  "name": "Theme Example",
  "colorSchemes": {
    "light": {
      "palette": {
        "primary": {
          "light": "var(--ns-palette-light-primary-light)",
          "main": "var(--ns-palette-light-primary-main)",
          "dark": "var(--ns-palette-light-primary-dark)",
          "lightChannel": "var(--ns-palette-light-primary-light-channel)",
          "mainChannel": "var(--ns-palette-light-primary-main-channel)",
          "darkChannel": "var(--ns-palette-light-primary-dark-channel)",
          "contrastText": "var(--ns-palette-light-primary-contrast-text)"
        },
...
      }
    },
    "dark": {
      "palette": {
        "primary": {
          "light": "var(--ns-palette-dark-primary-light)",
          "main": "var(--ns-palette-dark-primary-main)",
          "dark": "var(--ns-palette-dark-primary-dark)",
          "lightChannel": "var(--ns-palette-dark-primary-light-channel)",
          "mainChannel": "var(--ns-palette-dark-primary-main-channel)",
          "darkChannel": "var(--ns-palette-dark-primary-dark-channel)",
          "contrastText": "var(--ns-palette-dark-primary-contrast-text)"
        },
...
    }
  },
...
  "cssVariables": {
    "colorSchemeSelector": "class"
  },
  "palette": {}
}

This is heavily trimmed, but it should give the context. And yes, the theme itself pulls in css variables for the underlying primitives from our design system, and whilst it gets somewhat indirect, it actually works very nicely.

Then I change color scheme to dark, the main theme (the one attached to the ThemeProvider), becomes:

{
  "defaultColorScheme": "light",
  "name": "Dynamic Theme",
  "colorSchemeSelector": "class",
  "rootSelector": ":root",
  "vars": {
    "palette": {
      "primary": {
        "light": "var(--mui-palette-primary-light)",
        "main": "var(--mui-palette-primary-main)",
        "dark": "var(--mui-palette-primary-dark)",
        "lightChannel": "var(--mui-palette-primary-lightChannel)",
        "mainChannel": "var(--mui-palette-primary-mainChannel)",
        "darkChannel": "var(--mui-palette-primary-darkChannel)",
        "contrastText": "var(--mui-palette-primary-contrastText)",
        "contrastTextChannel": "var(--mui-palette-primary-contrastTextChannel)"
      },
...
    },
...
  },
  "palette": {
    "mode": "dark",
    "primary": {
      "light": "var(--ns-palette-dark-primary-light)",
      "main": "var(--ns-palette-dark-primary-main)",
      "dark": "var(--ns-palette-dark-primary-dark)",
      "lightChannel": "var(--ns-palette-dark-primary-light-channel)",
      "mainChannel": "var(--ns-palette-dark-primary-main-channel)",
      "darkChannel": "var(--ns-palette-dark-primary-dark-channel)",
      "contrastText": "var(--ns-palette-dark-primary-contrast-text)",
      "contrastTextChannel": "var(--ns-palette-dark-primary-contrast-text)"
    },
...
  "colorSchemes": {
    "light": {
      "palette": {
        "mode": "light",
        "primary": {
          "light": "var(--ns-palette-light-primary-light)",
          "main": "var(--ns-palette-light-primary-main)",
          "dark": "var(--ns-palette-light-primary-dark)",
          "lightChannel": "var(--ns-palette-light-primary-light-channel)",
          "mainChannel": "var(--ns-palette-light-primary-main-channel)",
          "darkChannel": "var(--ns-palette-light-primary-dark-channel)",
          "contrastText": "var(--ns-palette-light-primary-contrast-text)",
          "contrastTextChannel": "var(--ns-palette-light-primary-contrast-text)"
        },
...
    },
    "dark": {
      "palette": {
        "mode": "dark",
        "primary": {
          "light": "var(--ns-palette-dark-primary-light)",
          "main": "var(--ns-palette-dark-primary-main)",
          "dark": "var(--ns-palette-dark-primary-dark)",
          "lightChannel": "var(--ns-palette-dark-primary-light-channel)",
          "mainChannel": "var(--ns-palette-dark-primary-main-channel)",
          "darkChannel": "var(--ns-palette-dark-primary-dark-channel)",
          "contrastText": "var(--ns-palette-dark-primary-contrast-text)",
          "contrastTextChannel": "var(--ns-palette-dark-primary-contrast-text)"
        },
...
  },
  "cssVarPrefix": "mui"
}

With palette switching to dark as desired and referencing the underlying colorScheme.

If I just pass in mode = 'dark' to create them, I get this instead:

{
  "defaultColorScheme": "dark",
  "name": "Theme Example",
  "colorSchemeSelector": "class",
  "rootSelector": ":root",
  "vars": {
    "palette": {
      "primary": {
        "light": "var(--mui-palette-primary-light, #e3f2fd)",
        "main": "var(--mui-palette-primary-main, #90caf9)",
        "dark": "var(--mui-palette-primary-dark, #42a5f5)",
        "lightChannel": "var(--mui-palette-primary-lightChannel, 227 242 253)",
        "mainChannel": "var(--mui-palette-primary-mainChannel, 144 202 249)",
        "darkChannel": "var(--mui-palette-primary-darkChannel, 66 165 245)",
        "contrastText": "var(--mui-palette-primary-contrastText, rgba(0, 0, 0, 0.87))",
        "contrastTextChannel": "var(--mui-palette-primary-contrastTextChannel, 0 0 0)"
      },
...
    },
  },
  "palette": {
    "mode": "dark",
    "primary": {
      "main": "#90caf9",
      "light": "#e3f2fd",
      "dark": "#42a5f5",
      "contrastText": "rgba(0, 0, 0, 0.87)",
      "mainChannel": "144 202 249",
      "lightChannel": "227 242 253",
      "darkChannel": "66 165 245",
      "contrastTextChannel": "0 0 0"
    },
...
  "colorSchemes": {
    "dark": {
      "palette": {
        "mode": "dark",
        "primary": {
          "main": "#90caf9",
          "light": "#e3f2fd",
          "dark": "#42a5f5",
          "contrastText": "rgba(0, 0, 0, 0.87)",
          "mainChannel": "144 202 249",
          "lightChannel": "227 242 253",
          "darkChannel": "66 165 245",
          "contrastTextChannel": "0 0 0"
        },
...
    },
    "light": {
      "palette": {
        "mode": "light",
        "primary": {
          "light": "var(--ns-palette-light-primary-light)",
          "main": "var(--ns-palette-light-primary-main)",
          "dark": "var(--ns-palette-light-primary-dark)",
          "lightChannel": "var(--ns-palette-light-primary-light-channel)",
          "mainChannel": "var(--ns-palette-light-primary-main-channel)",
          "darkChannel": "var(--ns-palette-light-primary-dark-channel)",
          "contrastText": "var(--ns-palette-light-primary-contrast-text)",
          "contrastTextChannel": "var(--ns-palette-light-primary-contrast-text)"
        },
...
  },
  "cssVarPrefix": "mui",
}

Whatever theme I pass in as the mode gets overwritten.

Now, I do understand that this is [probably intentional, but it would be awfully useful if this overwriting of the colorScheme only happened when there wasn't already a colorScheme defined. That would make this much more flexible, and to my mind at least, more consistent

Motivation

I'm trying to display a theme inspector - similar to the mui 6 one, where I create a theme using cssVariables, and I can toggle between light and dark modes of the theme without changing the theme that's applied to the application.

I want some way to create a theme object that can be inspected in both light and dark modes that isn't tied to the DOM, as far as I can tell from digging into useCurrentColorScheme.js, there's no way to get a theme in a specific mode without it trying to edit the DOM. I want to be able to inspect the css variables, not apply them.

The closest is to use createTheme with palette.mode set to either light or dark, however that also initializes a default color scheme for that mode ignoring anything that you might already have set and then applying that scheme to the palette. This works as desired if you are using an autogenerated theme, but if you are trying to migrate from an existing theme and have a rather large set of values that you want to preserve, it ignores and overwrites them.

If I just display the main application theme, I can see it switch between light and dark mode and update the palette and it's working beautifully, I can inspect that value and get exactly the behavior that I'm after, but it's changing the application theme.

I want the same change to a theme but without tying it to a ThemeProvider or having it try and update the DOM.

Search keywords: CssVarProvider

siriwatknp commented 2 days ago

@ggascoigne I recommend moving palette: { mode: 'dark', … } over to colorSchemes completely.

The reason we override is to have a smooth migration from v5 to v6.

You no longer need to create a theme with mode = dark because you can provide dark tokens to colorSchemes.dark.palette.* directly. This make the theme creation static without binding to a state.

+ const theme = createTheme({ colorSchemes: { light: { … }, dark: { … } } });

function App() {
-  const appMode = useSomethingToGetTheMode();
-  const theme = createTheme({ palette: { mode: appMode }}}

  return <ThemeProvider theme={theme}>…
}
ggascoigne commented 2 days ago

Honestly I don't really care about the mechanism, and ended out with this because it sort of worked.

I want to create a palette with a specific active colorScheme so that it can be browsed, and do so without it being applied to the dom. Or change an existing palettes color mode, but again, without it applying to the DOM. Oh, and without it updating local storage.

As far as I tell, everything to do with colorSchemes is tightly bound to updating the DOM and updating local storage, and I want a way to avoid both.