Centralizing Your Design System with MUI's ThemeProvider and createTheme
Stop scattering sx props across your app. One createTheme object passed to a single ThemeProvider centralizes palette, typography, and global component defaults — here's a worked configuration plus the pitfalls.
23 Feb 2026, 19:30 UTC

The short answer
If you are overriding MUI component styles in more than a couple of places, stop scattering sx props and class overrides across your codebase. Create one theme object with createTheme, pass it to a single ThemeProvider at your app root, and put global component defaults in the theme's components section. Every MUI component in the tree then picks up your palette, typography, and component overrides through React context, with no prop drilling.
How the mechanism works
ThemeProvider is a thin wrapper around React context. It makes a theme object available to every MUI component below it, and MUI's styling engine (Emotion by default) reads that object when resolving values like primary.main or spacing(2). The createTheme function takes your partial configuration and deep-merges it with MUI's defaults, so you only specify what you want to change.
The theme has three areas you will touch most:
- palette — colors referenced by keys like
primary.mainorerror.light. - typography — font families, sizes, and weights per variant (h1, body1, button, and so on).
- components — global overrides for a component's default props and styles, keyed by the component's theme name (for example
MuiButton).
A worked configuration
The following example assumes MUI v5 or v6 with the default Emotion styling engine. Install the dependencies first:
npm install @mui/material @emotion/react @emotion/styledThen define the theme in its own file, for example src/theme.js:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: { main: '#0b5fff' },
secondary: { main: '#6b7280' },
},
typography: {
fontFamily: '"Inter", "Helvetica", "Arial", sans-serif',
button: { textTransform: 'none' },
},
components: {
MuiButton: {
defaultProps: {
disableElevation: true,
variant: 'contained',
},
styleOverrides: {
root: ({ theme }) => ({
borderRadius: theme.shape.borderRadius * 2,
}),
},
},
MuiTextField: {
defaultProps: {
size: 'small',
variant: 'outlined',
},
},
},
});
export default theme;Wire it up once at the application root, for example in src/main.jsx or wherever you render your root component:
import { ThemeProvider, CssBaseline } from '@mui/material';
import theme from './theme';
root.render(
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
);CssBaseline is MUI's normalized CSS reset; it also applies the theme's background and text colors to the document body. It is optional but usually wanted.
Now every <Button> renders contained, flat, and with the larger radius, and every <TextField> defaults to small and outlined — without touching individual call sites. You can still override per instance when genuinely needed:
<Button variant="outlined" sx={{ color: 'secondary.main' }}>
Cancel
</Button>The sx prop resolves secondary.main against the same theme object, so one-off exceptions stay consistent with the palette.
Verifying it works
Render a component that consumes the theme and check the computed styles in your browser's developer tools:
- Add a
<Button>Click</Button>somewhere visible and start the dev server. - Inspect the button. Its background color should compute to the RGB equivalent of
#0b5fff, and its border radius should be double the default shape radius. - Change
primary.mainin the theme file and confirm the button updates after hot reload. If it does not, the usual cause is a secondThemeProvider(or an un-themed test renderer) wrapping the component higher up.
For programmatic access inside your own components, use the useTheme hook:
import { useTheme } from '@mui/material/styles';
function StatusDot({ online }) {
const theme = useTheme();
return (
<span
style={{
backgroundColor: online
? theme.palette.success.main
: theme.palette.grey[400],
}}
/>
);
}Limits and common mistakes
Runtime theme changes re-render everything. Switching themes (for example light to dark mode) changes the context value, which re-renders all consumers. That is fine for an explicit user toggle, but do not create a new theme object on every render — memoize it:
const theme = useMemo(
() => createTheme({ palette: { mode } }),
[mode]
);Without memoization, every parent render produces a new theme identity and forces the whole tree to re-render.
Specificity conflicts in nested overrides. If you nest a second ThemeProvider deeper in the tree, MUI merges the outer and inner themes rather than replacing them. A styleOverrides rule in the inner theme that targets the same slot as the outer one can produce surprising results depending on rule order. Prefer one global theme and handle section-level variation with the sx prop or a scoped wrapper component.
The sx prop is not free in hot paths. Each sx object is processed by the styling engine at render time. For a row inside a virtualized list rendering thousands of times, the styled() API or a plain CSS class is cheaper because the styles are created once. Reserve sx for layout glue and low-frequency components.
Overriding what defaultProps already covers. A frequent mistake is writing a styleOverrides rule to undo a visual effect when a defaultProps entry does it cleanly — for example, setting disableElevation: true on buttons instead of overriding the box-shadow styles by hand. Check the component's prop list before writing CSS.
Version assumptions. The components key and the sx prop described here apply to MUI v5 and later. Material-UI v4 used overrides and props keys with a different structure, so check your installed version with npm list @mui/material before copying configuration from older tutorials.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.