Skip to content

Custom MUI library fails to get App Theme #47704

@George-Madeley

Description

@George-Madeley

Steps to reproduce

Cannot provide a live example as the code being developed is confidential for the company I work for (not my rules)

Current behavior

Custom library built on top of MUI does not use the theme provided my the app importing the library; instead using the default theme,

Expected behavior

The components in the library (built using MUI components) should use the theme provided by the app and not the default theme.

Context

I am creating a component library built on top of MUI. These are just a series of component we use frequently on our projects and instead of copying the source code by hand, we've decide to create a component library built on top of MUI and host on our local registry. The issue is that when we use the components, they are stuck to the default palette and refuse to update to the one our apps are providing. (i.e., if our app theme primary colour was red, all of the MUI components in our app would be red apart from any from our own library which remain the default blue). In addition, our library does not adapt to light/dark mode changes (forever stuck on the light mode style).

I've checked that there isn't another rogue <ThemeProvider /> in my app unintentionally providing the default theme (there is not). And I've added @mui/material to the peer dependencies to my library (and also DevDependencies so TypeScript stops complaining about unresolved dependencies). I've also told my libraries Vite config to externalise @mui/material. Nothing seems to be out of the ordinary but I cannot understand why the components from the library are not using the theme provided by the app.

I've attached what one of my built ESM components looks like in case anyone sports anything wrong

import { jsx } from "react/jsx-runtime";
import InfoIcon from "@mui/icons-material/Info";
import IconButton from "@mui/material/IconButton";
import { useCallback } from "react";
import useNoti from "../hooks/useNoti.mjs";
function InfoButton(props) {
  const { addNoti } = useNoti();
  const handleClick = useCallback(() => {
    addNoti(props.info ?? "", { ...props.options, variant: "info" });
  }, [addNoti, props.info, props.options]);
  return /* @__PURE__ */ jsx(
    IconButton,
    {
      onClick: handleClick,
      sx: props.info ? {} : { visibility: "hidden" },
      tabIndex: -1,
      children: /* @__PURE__ */ jsx(InfoIcon, { color: "info" })
    }
  );
}
export {
  InfoButton as default
};

I've also attached my vite.config.ts for the same reason:

import react from '@vitejs/plugin-react'
import { readFileSync } from 'fs';
import { glob } from "glob";
import { resolve } from 'path'
import { fileURLToPath } from 'url';
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'

const packageJson = JSON.parse(
  readFileSync(resolve(__dirname, 'package.json'), 'utf-8')
)

// Get all peer dependencies
const peerDeps = Object.keys(packageJson.peerDependencies || {})

console.log(peerDeps)

// Create external matcher
const external = [
  ...peerDeps,
  'react/jsx-runtime',
  'react/jsx-dev-runtime',
  // Match all subpath imports from peer dependencies
  new RegExp(`^(${peerDeps.map(dep => dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(/.*)?$`)
]

export default defineConfig({
  plugins: [
    react({
      jsxRuntime: 'automatic',
    }),
    dts({
      outDir: "dist/types",
      insertTypesEntry: true,
      include: ['src/**/*'],
      exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.stories.tsx']
    })
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: '@atl/ui',
    },
    rollupOptions: {
      external, // [/node_modules/],
      input: Object.fromEntries(
        glob.sync('src/**/*.{ts,tsx}', {
          ignore: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.stories.tsx']
        }).map(file => {
          // Remove src/ prefix and file extension for entry key
          const filename = file.replace(/^src[/|\\]/, '').replace(/\.[^.]+$/, '');
          // Provide absolute path for entry value
          const path = fileURLToPath(new URL(file, import.meta.url))
          return [filename, path]
        })
      ),
      output: [
        {
          format: 'es',
          dir: 'dist/esm',
          entryFileNames: '[name].mjs',
          preserveModules: true,
          preserveModulesRoot: 'src',
          interop: 'auto',
          generatedCode: {
            symbols: true
          }
        },
        {
          format: 'cjs',
          dir: 'dist/cjs',
          entryFileNames: '[name].cjs',
          preserveModules: true,
          preserveModulesRoot: 'src',
          exports: 'named',
          interop: 'auto',
          generatedCode: {
            symbols: true
          }
        }
      ]
    },
    sourcemap: false,
    emptyOutDir: true,
    minify: false,
  }
})

Your environment

npx @mui/envinfo

System:
OS: Windows 11 10.0.26200
Binaries:
Node: 24.13.0 - C:\Program Files\nodejs\node.EXE
npm: 11.5.1 - C:\Program Files\nodejs\npm.CMD
Browsers:
Chrome: 144.0.7559.110
Edge: Chromium (140.0.3485.54)
npmPackages:
@emotion/react: 11.14.0
@emotion/styled: 11.14.1
@mui/core-downloads-tracker: 7.3.7
@mui/icons-material: 7.3.7
@mui/material: 7.3.7
@mui/material-nextjs: 7.3.7
@mui/private-theming: 7.3.7
@mui/styled-engine: 7.3.7
@mui/system: 7.3.7
@mui/types: 7.4.10
@mui/utils: 7.3.7
@mui/x-data-grid: 8.26.0
@mui/x-date-pickers: 8.26.0
@mui/x-internals: 8.26.0
@mui/x-virtualizer: 0.3.3
@toolpad/core: 0.16.0
@toolpad/utils: 0.16.0
@types/react: ^19.1.8 => 19.2.10
react: 19.2.0
react-dom: 19.2.0
typescript: ^5.8.3 => 5.9.3

Search keywords: theme

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions