From mui-expert
MUI theming system — createTheme, palette, typography, dark mode, component overrides, TypeScript augmentation, and design tokens
npx claudepluginhub markus41/claude --plugin mui-expertThis skill is limited to using the following tools:
The `createTheme` function is the entry point for all MUI customization. It accepts a deeply partial theme object and merges it with the defaults.
Provides Ktor server patterns for routing DSL, plugins (auth, CORS, serialization), Koin DI, WebSockets, services, and testApplication testing.
Conducts multi-source web research with firecrawl and exa MCPs: searches, scrapes pages, synthesizes cited reports. For deep dives, competitive analysis, tech evaluations, or due diligence.
Provides demand forecasting, safety stock optimization, replenishment planning, and promotional lift estimation for multi-location retailers managing 300-800 SKUs.
The createTheme function is the entry point for all MUI customization. It accepts a deeply partial theme object and merges it with the defaults.
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: { ... },
typography: { ... },
spacing: 8, // base spacing unit in px (default: 8)
shape: { borderRadius: 8 },
breakpoints: { ... },
shadows: [...],
transitions: { ... },
zIndex: { ... },
});
const theme = createTheme({
palette: {
primary: {
light: '#757ce8',
main: '#3f50b5',
dark: '#002884',
contrastText: '#fff',
},
secondary: {
light: '#ff7961',
main: '#f44336',
dark: '#ba000d',
contrastText: '#000',
},
error: { main: '#d32f2f' },
warning: { main: '#ed6c02' },
info: { main: '#0288d1' },
success: { main: '#2e7d32' },
},
});
MUI auto-generates light, dark, and contrastText from main if you omit them.
const theme = createTheme({
palette: {
// augmentColor adds light/dark/contrastText automatically
neutral: theme.palette.augmentColor({
color: { main: '#64748b' },
name: 'neutral',
}),
brand: theme.palette.augmentColor({
color: { main: '#6366f1', light: '#818cf8', dark: '#4f46e5' },
name: 'brand',
}),
},
});
Use a two-step createTheme call when augmentColor needs the base theme:
let theme = createTheme();
theme = createTheme(theme, {
palette: {
salmon: theme.palette.augmentColor({
color: { main: '#FF5733' },
name: 'salmon',
}),
},
});
palette: {
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
text: {
primary: 'rgba(0,0,0,0.87)',
secondary: 'rgba(0,0,0,0.6)',
disabled: 'rgba(0,0,0,0.38)',
},
divider: 'rgba(0,0,0,0.12)',
action: {
active: 'rgba(0,0,0,0.54)',
hover: 'rgba(0,0,0,0.04)',
selected: 'rgba(0,0,0,0.08)',
disabled: 'rgba(0,0,0,0.26)',
disabledBackground: 'rgba(0,0,0,0.12)',
focus: 'rgba(0,0,0,0.12)',
},
},
const theme = createTheme({
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
fontSize: 14, // base font size (rem calculation root)
htmlFontSize: 16, // <html> font size for rem calculations
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 700,
h1: { fontSize: '2.5rem', fontWeight: 700, lineHeight: 1.2, letterSpacing: '-0.01562em' },
h2: { fontSize: '2rem', fontWeight: 700, lineHeight: 1.3 },
h3: { fontSize: '1.75rem', fontWeight: 600, lineHeight: 1.3 },
h4: { fontSize: '1.5rem', fontWeight: 600, lineHeight: 1.4 },
h5: { fontSize: '1.25rem', fontWeight: 600, lineHeight: 1.5 },
h6: { fontSize: '1rem', fontWeight: 600, lineHeight: 1.6 },
subtitle1: { fontSize: '1rem', fontWeight: 400, lineHeight: 1.75 },
subtitle2: { fontSize: '0.875rem', fontWeight: 500, lineHeight: 1.57 },
body1: { fontSize: '1rem', fontWeight: 400, lineHeight: 1.5 },
body2: { fontSize: '0.875rem', fontWeight: 400, lineHeight: 1.43 },
button: { fontSize: '0.875rem', fontWeight: 600, textTransform: 'none' }, // disable ALL_CAPS
caption: { fontSize: '0.75rem', fontWeight: 400, lineHeight: 1.66 },
overline: { fontSize: '0.75rem', fontWeight: 400, textTransform: 'uppercase', letterSpacing: '0.08333em' },
},
});
typography: {
h1: {
fontSize: '2rem',
[theme.breakpoints.up('md')]: { fontSize: '3rem' },
[theme.breakpoints.up('lg')]: { fontSize: '4rem' },
},
},
The spacing scale is factor-based. Default unit is 8px.
const theme = createTheme({ spacing: 8 }); // default
theme.spacing(1) // '8px'
theme.spacing(2) // '16px'
theme.spacing(0.5) // '4px'
theme.spacing(1, 2) // '8px 16px'
theme.spacing(1, 2, 3, 4) // '8px 16px 24px 32px'
// Custom spacing function
const theme = createTheme({
spacing: (factor: number) => `${0.25 * factor}rem`,
});
const theme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
// Add custom breakpoints:
mobile: 0,
tablet: 640,
laptop: 1024,
desktop: 1200,
},
},
});
// Usage in sx
<Box sx={{ fontSize: { xs: '1rem', md: '1.5rem', lg: '2rem' } }} />
// Usage in styles
theme.breakpoints.up('md') // '@media (min-width:900px)'
theme.breakpoints.down('md') // '@media (max-width:899.95px)'
theme.breakpoints.between('sm', 'md') // '@media (min-width:600px) and (max-width:899.95px)'
theme.breakpoints.only('md') // '@media (min-width:900px) and (max-width:1199.95px)'
const theme = createTheme({
shape: {
borderRadius: 8, // default: 4
},
// shadows[0] = 'none', shadows[1-24] = elevation levels
shadows: [
'none',
'0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12)',
// ... (24 levels total — override individual elevations selectively)
...Array(23).fill('none'),
],
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)',
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
sharp: 'cubic-bezier(0.4, 0, 0.6, 1)',
},
},
zIndex: {
mobileStepper: 1000,
fab: 1050,
speedDial: 1050,
appBar: 1100,
drawer: 1200,
modal: 1300,
snackbar: 1400,
tooltip: 1500,
},
});
import { ThemeProvider, CssBaseline } from '@mui/material';
import { createTheme } from '@mui/material/styles';
const theme = createTheme({ ... });
function App() {
return (
<ThemeProvider theme={theme}>
{/* CssBaseline normalizes browser styles and sets body background */}
<CssBaseline />
<YourApp />
</ThemeProvider>
);
}
const darkTheme = createTheme({
palette: {
mode: 'dark',
// MUI auto-adjusts all palette colors for dark mode
// Override as needed:
primary: { main: '#90caf9' },
background: {
default: '#121212',
paper: '#1e1e1e',
},
},
});
import React, { createContext, useContext, useMemo, useState } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
// Create context
const ColorModeContext = createContext({ toggleColorMode: () => {} });
export function useColorMode() {
return useContext(ColorModeContext);
}
export function ColorModeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const colorMode = useMemo(
() => ({
toggleColorMode: () =>
setMode((prev) => (prev === 'light' ? 'dark' : 'light')),
}),
[]
);
const theme = useMemo(
() =>
createTheme({
palette: { mode },
}),
[mode]
);
return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
}
// In a component:
function DarkModeToggle() {
const { toggleColorMode } = useColorMode();
return <IconButton onClick={toggleColorMode}><Brightness4Icon /></IconButton>;
}
import useMediaQuery from '@mui/material/useMediaQuery';
function App() {
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const [mode, setMode] = useState<'light' | 'dark'>(
prefersDark ? 'dark' : 'light'
);
const theme = useMemo(() => createTheme({ palette: { mode } }), [mode]);
// ...
}
Override CSS for specific component slots:
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: '8px',
textTransform: 'none',
fontWeight: 600,
},
contained: {
boxShadow: 'none',
'&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.15)' },
},
sizeLarge: {
padding: '12px 24px',
fontSize: '1rem',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
},
},
},
},
MuiPaper: {
styleOverrides: {
root: ({ theme }) => ({
backgroundImage: 'none',
...(theme.palette.mode === 'dark' && {
backgroundColor: theme.palette.grey[900],
}),
}),
},
},
},
});
Set default prop values for all instances of a component:
const theme = createTheme({
components: {
MuiButton: {
defaultProps: {
variant: 'contained',
disableElevation: true,
},
},
MuiTextField: {
defaultProps: {
variant: 'outlined',
size: 'small',
fullWidth: true,
},
},
MuiCircularProgress: {
defaultProps: {
size: 24,
thickness: 4,
},
},
MuiLink: {
defaultProps: {
underline: 'hover',
},
},
},
});
Add new named variants to existing components:
const theme = createTheme({
components: {
MuiButton: {
variants: [
{
props: { variant: 'dashed' },
style: {
border: '2px dashed currentColor',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.04)',
},
},
},
{
props: { variant: 'gradient' },
style: ({ theme }) => ({
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
color: theme.palette.primary.contrastText,
border: 'none',
}),
},
],
},
},
});
// Merge two themes
import { deepmerge } from '@mui/utils';
const baseTheme = createTheme({ ... });
const extendedTheme = createTheme(deepmerge(baseTheme, {
typography: { h1: { fontSize: '3rem' } },
}));
// Nested ThemeProvider (child theme overrides parent locally)
function AdminPanel() {
const parentTheme = useTheme();
const adminTheme = createTheme(parentTheme, {
palette: {
primary: { main: '#dc2626' }, // red for admin
},
});
return (
<ThemeProvider theme={adminTheme}>
<AdminUI />
</ThemeProvider>
);
}
Extend the theme types to support custom colors and variables:
// theme-augmentation.d.ts (or in your theme file)
import '@mui/material/styles';
import '@mui/material/Button';
declare module '@mui/material/styles' {
// Add custom palette colors
interface Palette {
neutral: Palette['primary'];
brand: Palette['primary'];
custom: {
gradientStart: string;
gradientEnd: string;
};
}
interface PaletteOptions {
neutral?: PaletteOptions['primary'];
brand?: PaletteOptions['primary'];
custom?: {
gradientStart?: string;
gradientEnd?: string;
};
}
// Add custom theme variables
interface Theme {
custom: {
navHeight: number;
sidebarWidth: number;
};
}
interface ThemeOptions {
custom?: {
navHeight?: number;
sidebarWidth?: number;
};
}
// Extend typography variants
interface TypographyVariants {
code: React.CSSProperties;
label: React.CSSProperties;
}
interface TypographyVariantsOptions {
code?: React.CSSProperties;
label?: React.CSSProperties;
}
}
// Allow usage of custom variants on Typography component
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
code: true;
label: true;
}
}
// Allow usage of custom palette colors on Button
declare module '@mui/material/Button' {
interface ButtonPropsColorOverrides {
neutral: true;
brand: true;
}
}
With augmentation in place, usage is fully typed:
const theme = createTheme({
palette: {
neutral: theme.palette.augmentColor({ color: { main: '#64748b' }, name: 'neutral' }),
custom: { gradientStart: '#667eea', gradientEnd: '#764ba2' },
},
custom: { navHeight: 64, sidebarWidth: 240 },
typography: {
code: { fontFamily: 'monospace', fontSize: '0.85rem', backgroundColor: 'rgba(0,0,0,0.06)' },
},
});
// Fully typed:
<Button color="neutral">Neutral Button</Button>
<Typography variant="code">const x = 1;</Typography>
const navH = theme.custom.navHeight; // typed as number
Centralize all design decisions as named tokens, then reference them throughout the theme:
// design-tokens.ts
export const tokens = {
color: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
900: '#1e3a8a',
},
neutral: {
50: '#f8fafc',
100: '#f1f5f9',
500: '#64748b',
900: '#0f172a',
},
semantic: {
success: '#16a34a',
warning: '#d97706',
error: '#dc2626',
info: '#0284c7',
},
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
'2xl': 48,
'3xl': 64,
},
typography: {
fontFamily: {
sans: '"Inter", system-ui, sans-serif',
mono: '"JetBrains Mono", "Fira Code", monospace',
},
scale: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
},
},
radius: {
sm: 4,
md: 8,
lg: 12,
xl: 16,
full: 9999,
},
} as const;
// theme.ts
import { createTheme } from '@mui/material/styles';
import { tokens } from './design-tokens';
export const theme = createTheme({
palette: {
primary: {
light: tokens.color.brand[100],
main: tokens.color.brand[500],
dark: tokens.color.brand[900],
},
success: { main: tokens.color.semantic.success },
warning: { main: tokens.color.semantic.warning },
error: { main: tokens.color.semantic.error },
info: { main: tokens.color.semantic.info },
},
typography: {
fontFamily: tokens.typography.fontFamily.sans,
h1: { fontSize: tokens.typography.scale['4xl'] },
h2: { fontSize: tokens.typography.scale['3xl'] },
h3: { fontSize: tokens.typography.scale['2xl'] },
body1: { fontSize: tokens.typography.scale.base },
body2: { fontSize: tokens.typography.scale.sm },
},
shape: { borderRadius: tokens.radius.md },
spacing: (factor: number) => `${tokens.spacing.xs * factor}px`,
});
// theme/index.ts
import { createTheme, responsiveFontSizes } from '@mui/material/styles';
let theme = createTheme({
palette: {
mode: 'light',
primary: { main: '#2563eb' },
secondary: { main: '#7c3aed' },
error: { main: '#dc2626' },
warning: { main: '#d97706' },
info: { main: '#0284c7' },
success: { main: '#16a34a' },
background: { default: '#f8fafc', paper: '#ffffff' },
},
typography: {
fontFamily: '"Inter", "Roboto", sans-serif',
fontWeightBold: 700,
button: { textTransform: 'none', fontWeight: 600 },
},
shape: { borderRadius: 8 },
components: {
MuiButton: {
defaultProps: { disableElevation: true },
styleOverrides: {
root: { borderRadius: 8, padding: '8px 20px' },
},
},
MuiCard: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: { border: '1px solid', borderColor: 'divider', borderRadius: 12 },
},
},
},
});
// Automatically scale typography for different screen sizes
theme = responsiveFontSizes(theme);
export default theme;
Ship a branded theme as an npm package for multi-app reuse.
// @your-org/mui-theme/src/index.ts
import { createTheme, type ThemeOptions } from '@mui/material/styles';
import { tokens } from './tokens';
import { componentOverrides } from './components';
// Base branded theme
export const brandedThemeOptions: ThemeOptions = {
palette: {
primary: { main: tokens.color.brand[500] },
secondary: { main: tokens.color.brand[700] },
},
typography: {
fontFamily: tokens.typography.fontFamily.sans,
button: { textTransform: 'none', fontWeight: 600 },
},
shape: { borderRadius: 8 },
components: componentOverrides,
};
// Create with optional per-app overrides
export function createBrandedTheme(...overrides: ThemeOptions[]) {
return createTheme(brandedThemeOptions, ...overrides);
}
// Pre-built themes
export const lightTheme = createBrandedTheme();
export const darkTheme = createBrandedTheme({
palette: { mode: 'dark' },
});
Consumer app layers overrides on top:
import { createBrandedTheme } from '@your-org/mui-theme';
const appTheme = createBrandedTheme({
components: {
MuiButton: {
styleOverrides: {
root: {
'&:hover': { transform: 'translateY(-2px)' },
},
},
},
},
});
Array styleOverrides merging: When extending, use arrays to preserve base styles:
root: [
brandedComponents.MuiButton.styleOverrides.root,
{ '&:hover': { transform: 'translateY(-2px)' } },
]
Encode design decisions in the theme and enforce at compile time via module augmentation.
// Disable 'text' variant for Buttons in your design system
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
text: false; // ← compile error if used
dashed: true; // ← new custom variant
gradient: true; // ← new custom variant
}
interface ButtonPropsColorOverrides {
inherit: false; // ← disable inherit
brand: true; // ← custom brand color
neutral: true; // ← custom neutral color
}
interface ButtonPropsSizeOverrides {
extraLarge: true; // ← custom size
}
}
Now <Button variant="text"> is a TypeScript error — design rules enforced at compile time.
// Register MuiStat in the theme like a built-in component
declare module '@mui/material/styles' {
interface Components {
MuiStat?: {
defaultProps?: Partial<StatProps>;
styleOverrides?: {
root?: any;
value?: any;
label?: any;
};
variants?: Array<{
props: Partial<StatProps>;
style: any;
}>;
};
}
}
// Now you can configure it globally via theme
const theme = createTheme({
components: {
MuiStat: {
defaultProps: { trend: 'flat' },
styleOverrides: {
root: ({ theme }) => ({
backgroundColor: theme.palette.background.paper,
'&:hover': { borderColor: theme.palette.primary.main },
}),
},
},
},
});
// Complex conditional variant matching
const theme = createTheme({
components: {
MuiButton: {
variants: [
{
// Function-based matching for complex rules
props: (props) =>
props.variant === 'dashed' && props.color !== 'secondary',
style: {
border: '2px dashed currentColor',
backgroundColor: 'transparent',
},
},
],
},
},
});