[material-ui][theme scoping] Two 'sandboxed' @mui/material based themes in use at the same time
@siriwatknp is already working on this.
Since Apr 23, 2025.
- Dominant language
- JavaScript
- Stars
- 99.1k
- Forks
- 32.5k
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 106
Description
Summary
In order to make it easier to migrate between two @mui/material based design systems, support multiple sandboxed @mui/material themes.
I do not know if this is feasible or a good idea. I faced this issue while trying to use a design system based on @mui/material in a project that already uses @mui/material. The merging of themes that happens by default creates a hard to reason about web of interdependence I haven't been able to reconcile.
Examples
In essence I would benefit if the following scenario were possible:
import { createTheme, ThemeProvider, Typography } from "@mui/material";
import { DesignSystemProvider, Typography as DsTypography } from "@org/design-system";
const appTheme = createTheme();
export const App = ({ children }) => {
<ThemeProvider theme={appTheme}>
<DesignSystemProvider>
<Typography variant="h1">Hello World</Typography>
<DsTypography variant="h1">Hello World</DsTypography>
</DesignSystemProvider>
</ThemeProvider>
};
In this example DesignSystemProvider happens to provide another @mui/material theme:
import type { PropsWithChildren } from 'react';
import { ThemeProvider, createTheme } from '@mui/material';
const dsTheme = createTheme(/* ...customisations */);
export const Provider: React.FC<PropsWithChildren> = ({ children }) => (
<ThemeProvider theme={dsTheme}>
{children}
</ThemeProvider>
);
and DsTypography is @mui/material based
import { Typography } from "@mui/material";
export const Typography = (props) => (
// Assume we customise variants etc. here
<Typography {...props}>
{children}
</Typography>
);
I would like this instance of typography
<Typography variant="h1">Hello World</Typography>
to ignore dsTheme while making use of appTheme. And this instance:
<DsTypography variant="h1">Hello World</DsTypography>
to ignore appTheme while making use of dsTheme.
The default behaviour is that the two themes get merged and dsTheme potentially overwrites the appTheme. AFAIK, it's not possible to avoid this merge if you are using two @mui/material themes (@mui/styled does not count).
There is an existing concept in @mui/material called "Theme Scoping". Could it be extended to allow scoping between two different @mui/material themes?
The idea is that instead of just the THEME_ID exported by @mui/material, I could use a custom theme ID that "sandboxes" the theme.
import type { PropsWithChildren } from 'react';
import { ThemeProvider, createTheme } from '@mui/material';
const DESIGN_SYSTEM_THEME_ID = "ds";
const dsTheme = createTheme(/* ...customisations */);
export const Provider: React.FC<PropsWithChildren> = ({ children }) => (
<ThemeProvider theme={{ [DESIGN_SYSTEM_THEME_ID]: dsTheme }}>
{children}
</ThemeProvider>
);
When I use components from @mui/material in the design system package, I would need to tell them to look for the theme under a specific key. Otherwise the components would try to use the theme without an ID--appTheme. I don't personally have a great grasp on how this API could be orchestrated. My own imagination can come up with a generic prop accepted by all @mui/system based components:
import { Typography } from "@mui/material";
export const Typography = (props) => (
// Assume we customise variants etc. here
<Typography themeId="ds" {...props}>
{children}
</Typography>
);
Could be verbose. It'd be more ideal if the information could be provided as a setting for the entire design system package. How that would work, I do not know.
Motivation
This could be a relevant feature for re-implementing @mui/joy as a theme for @mui/material instead of a "sibling" design system as has been speculated for instance here when @mui/joy and @mui/material are in use at the same time (example).
I am working on a legacy monolith project which uses @mui/material as a "component library". I want to enforce better separation of concerns and better reuse by picking out some usecases from the monolith into a separate design system. In order to avoid added complexity during the migration, I want to implement the design system with @mui/material. This way changes to how components work can be minimised--minimising the chance for regressions. Otherwise I need to re-implement @mui/material features during the transition. It would increase development resource use, but also increases regression risk.
I want to place the design-system in a separate package so that I have more control over the API contract. That way I can control long term risk. If we need to migrate away from @mui/material, we can potentially do the migration with more limited API changes as we can constrain the API now without much chance for regression. I am concerned about themes being merged, becoming interdependent and leaking concepts. I would prefer to provide the design system as a black box solution. I don't want that consumers of the design system need to account for how the design system theme may impact their @mui/material theme and vice versa. I'd prefer to have some sort of sandboxing in place so that the two themes can coexist without knowing anything about each other.
I have found these concepts while going over the documentation:
- Theme scoping: Doesn't allow two
@mui/materialthemes to coexist--only allows a@mui/materialtheme to coexist with some otheremotionbased theme - themeId:
@mui/materialonly supports the built inTHEME_IDwhich doesn't work for sandboxing. I would need to re-implement@mui/materialfeatures with@mui/styled(etc.) - @mui/joy: Provides an example of how to build a design system by composing packages in the
@muiecosystem. However, the approach require re-implementation and I would prefer to avoid that. It's "one level" deeper than I would prefer to go--in my scenario I would prefer to use features implemented in the@mui/materiallayer
I have one proof of concept where I have adopted the approach in @mui/joy. I have:
- Used
@mui/systemto implement a theme with a custom theme ID - Crated my own style utilities (such as
styled) that use the custom theme ID - Used those utilities to re-implement the
Typographycomponent. The result is a sandboxed design system. It's not bad, but it does require re-implementation. For instance in the case of theTypographycomponent, I have to create aTypographyRootthat uses thestyledutility with the correct theme ID, and then wrap it with another component that, among other things, re-implement the variant to component logic. If we expand to think about the other components where I'd need to re-implement things--it'd make for a lot of work.
Search keywords: material-ui, multiple themes, theme conflict, theme scoping, sandboxing
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.