I'm encountering an issue with the mounting of MaterialUI styles within the shadow DOM when using Vite as a bundler. I've followed the steps outlined in the MaterialUI documentation (https://mui.com/material-ui/guides/shadow-dom/) to address CSS bleeding by incorporating shadow DOM. Locally, everything works as expected in Vite's dev mode.
However, when I build the web-components and externally bundle MaterialUI along with other resources, the styles seem to mount outside the shadow DOM, causing unexpected behavior. I've referred to the documentation at https://emotion.sh/docs/@emotion/cache#container, which suggests providing a container for styles within the shadow DOM context.
I've also adjusted the theme to specify a new mounting point for certain components from MaterialUI, as advised in the documentation.
The strange part is that everything works as expected locally in dev mode, but after building the web-components, the styles mount incorrectly. I suspect the issue may be related to the external bundling of MaterialUI, as removing it from external dependencies and bundling it directly with my web-component resolves the problem.
Any insights or suggestions on how to ensure correct style mounting within the shadow DOM when externally bundling MaterialUI with Vite would be greatly appreciated. Thank you!
I was able to produce a minimal example: https://github.com/beuluis/MUI-Shadow-DOM-CacheProvider-Broken-Theme
vite.config.ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import viteTsconfigPaths from 'vite-tsconfig-paths';
const environmentName = process.env.npm_package_name;
if (!environmentName) {
throw new Error('Missing package name in environment variables. Vue CLI should set this.');
}
const name = environmentName.split('/').pop();
export default defineConfig({
plugins: [react(), viteTsconfigPaths()],
build: {
sourcemap: true,
rollupOptions: {
external: ['@emotion/react', '@emotion/styled', '@mui/material', 'react', 'react-dom'],
output: {
format: 'iife',
assetFileNames: `${name}.[hash].[extname]`,
chunkFileNames: `${name}.[hash].chunk.js`,
entryFileNames: `${name}.min.js`,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'@mui/material': 'MaterialUI',
'@emotion/react': 'emotionReact',
'@emotion/styled': 'emotionStyled',
},
},
},
outDir: 'dist',
},
});
App.tsx
import { StrictMode, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { createTheme, ThemeProvider } from '@mui/material';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Loading } from '@company/partner-ui'; // company component lib
import { theme } from '@company/partner-ui/dist/theme'; // company theme
import { NamespacedRouter } from '@company/react-namespaced-router'; // company router to support specific url handling
import { useWidget } from '@company/web-component-core'; // company core lib for web-components. takes over mounting and attribute watching etc
import { ApiContextProvider } from './contexts/api';
import { useLanguage } from './hooks/useLanguage';
import { PartnerData } from './pages/PartnerData/PartnerData';
export interface AppProperties {
readonly apiBaseUrl: string;
readonly locale: string;
readonly routerName?: string;
readonly basename?: string;
}
export const App = ({
apiBaseUrl,
basename,
locale,
routerName = 'partner-data-frontend',
}: AppProperties) => {
useLanguage(locale);
// This all is in out lib that provides also useWidget(); I wrote a rough example how it will create those containers.
// const root = this.shadowRoot ?? this;
// const containerElement = create('root');
// const stylesContainer = create('styles');
// const customStylesPoint = create('custom-styles');
// root.append(stylesContainer, containerElement);
// stylesContainer.append(customStylesPoint);
const { containerElement, stylesContainer } = useWidget();
const cache = createCache({
key: 'css',
prepend: true,
container: stylesContainer,
});
const shadowDomTheme = createTheme(theme, {
components: {
MuiPopover: {
defaultProps: {
container: containerElement,
},
},
MuiPopper: {
defaultProps: {
container: containerElement,
},
},
MuiModal: {
defaultProps: {
container: containerElement,
},
},
},
});
const queryClient = new QueryClient();
return (
<StrictMode>
<CacheProvider value={cache}>
<ThemeProvider theme={shadowDomTheme}>
<Suspense fallback={<Loading />}>
<QueryClientProvider client={queryClient}>
<NamespacedRouter name={routerName} basename={basename}>
<ApiContextProvider baseUrl={apiBaseUrl}>
<Routes>
<Route path="/">
<Route index element={<PartnerData />} />
</Route>
</Routes>
</ApiContextProvider>
</NamespacedRouter>
</QueryClientProvider>
</Suspense>
</ThemeProvider>
</CacheProvider>
</StrictMode>
);
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material@5/umd/material-ui.production.min.js"></script>
<script src="https://unpkg.com/@emotion/react@11/dist/emotion-react.umd.min.js"></script>
<script src="https://unpkg.com/@emotion/styled@11/dist/emotion-styled.umd.min.js"></script>
<title>demo page</title>
<script type="module" src="src/App.tsx"></script>
</head>
<body>
<!-- This gets mounted by our company web-component-core lib -->
<partner-data-frontend api-base-url="" locale="en-GB"></partner-data-frontend>
</body>
</html>
I tried:
- Test out a different bundler. Failed because this bundler did not support all needed
- Tried to Minimize code. Failed
- Tried to bundle MUI and not have it external. Worked but not really a solution
- Tried different mounting containers. Failed
- Tried it without useMemo