Claude
Skills
Sign in
Back

white-label

Included with Lifetime
$97 forever

MUI multi-theme and white-label systems — nested ThemeProvider, dynamic theme switching, tenant-specific branding, and theme composition

General

What this skill does


# MUI White-Label and Multi-Theme Systems

## Nested ThemeProvider

MUI's `ThemeProvider` can be nested. Inner providers merge with or override the outer
theme. Use this to scope different visual treatments to different sections of the app.

```tsx
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';

const mainTheme = createTheme({
  palette: {
    primary: { main: '#1976d2' },
    background: { default: '#fafafa' },
  },
});

// Admin sidebar uses a dark theme, scoped to its subtree
const adminSidebarTheme = createTheme({
  palette: {
    mode: 'dark',
    primary: { main: '#90caf9' },
    background: { default: '#1e1e1e', paper: '#2d2d2d' },
  },
});

export function AppShell() {
  return (
    <ThemeProvider theme={mainTheme}>
      <CssBaseline />
      <Box sx={{ display: 'flex' }}>
        {/* Dark sidebar — scoped theme */}
        <ThemeProvider theme={adminSidebarTheme}>
          <Paper sx={{ width: 280, minHeight: '100vh', p: 2 }}>
            <Typography variant="h6">Admin</Typography>
            <Button variant="contained">Dashboard</Button>
          </Paper>
        </ThemeProvider>

        {/* Main content — inherits mainTheme */}
        <Box sx={{ flex: 1, p: 3 }}>
          <Typography variant="h4">Welcome</Typography>
          <Button variant="contained">Main Action</Button>
        </Box>
      </Box>
    </ThemeProvider>
  );
}
```

### Nested Theme with Callback (Merge Instead of Replace)

Pass a function to `ThemeProvider` to receive the outer theme and merge selectively:

```tsx
<ThemeProvider theme={mainTheme}>
  <ThemeProvider
    theme={(outerTheme) =>
      createTheme({
        ...outerTheme,
        palette: {
          ...outerTheme.palette,
          primary: { main: '#e91e63' },  // override only primary
        },
      })
    }
  >
    <Button variant="contained">Pink Button, rest of theme inherited</Button>
  </ThemeProvider>
</ThemeProvider>
```

---

## Dynamic Theme Loading

Load theme configuration from an API or database at runtime. This is the foundation
of any white-label system.

```tsx
import { ThemeProvider, createTheme, type ThemeOptions } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import { useState, useEffect, useMemo, type ReactNode } from 'react';

interface BrandConfig {
  primaryColor: string;
  secondaryColor: string;
  fontFamily: string;
  borderRadius: number;
  logoUrl: string;
  mode: 'light' | 'dark';
}

function buildThemeOptions(brand: BrandConfig): ThemeOptions {
  return {
    palette: {
      mode: brand.mode,
      primary: { main: brand.primaryColor },
      secondary: { main: brand.secondaryColor },
    },
    typography: {
      fontFamily: brand.fontFamily,
    },
    shape: {
      borderRadius: brand.borderRadius,
    },
  };
}

async function fetchBrandConfig(tenantId: string): Promise<BrandConfig> {
  const res = await fetch(`/api/tenants/${tenantId}/brand`);
  if (!res.ok) throw new Error(`Failed to load brand config for ${tenantId}`);
  return res.json();
}

interface DynamicThemeProviderProps {
  tenantId: string;
  children: ReactNode;
}

export function DynamicThemeProvider({ tenantId, children }: DynamicThemeProviderProps) {
  const [brandConfig, setBrandConfig] = useState<BrandConfig | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchBrandConfig(tenantId)
      .then(setBrandConfig)
      .catch((err) => setError(err.message));
  }, [tenantId]);

  const theme = useMemo(
    () => (brandConfig ? createTheme(buildThemeOptions(brandConfig)) : null),
    [brandConfig],
  );

  if (error) return <Box sx={{ p: 4, color: 'error.main' }}>Theme load failed: {error}</Box>;
  if (!theme) {
    return (
      <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
        <CircularProgress />
      </Box>
    );
  }

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      {children}
    </ThemeProvider>
  );
}
```

---

## Theme Composition

Use `deepmerge` (shipped with MUI as `@mui/utils/deepmerge`) to layer tenant
overrides on top of a base theme. This ensures tenants only override what they need
and inherit everything else.

```tsx
import { createTheme, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';

// Shared base — every tenant gets this as the foundation
const baseThemeOptions: ThemeOptions = {
  typography: {
    fontFamily: '"Inter", "Helvetica", "Arial", sans-serif',
    h1: { fontSize: '2.5rem', fontWeight: 700 },
    h2: { fontSize: '2rem', fontWeight: 600 },
    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: 'rgba(0,0,0,0.12)' },
      },
    },
    MuiTextField: {
      defaultProps: { variant: 'outlined', size: 'small' },
    },
  },
};

// Tenant-specific overrides — only the diff
const acmeOverrides: ThemeOptions = {
  palette: {
    primary: { main: '#ff5722' },
    secondary: { main: '#ff9800' },
  },
  typography: {
    fontFamily: '"Poppins", sans-serif',
  },
  components: {
    MuiButton: {
      styleOverrides: {
        root: { borderRadius: 24 },   // pill buttons for Acme
      },
    },
  },
};

const globexOverrides: ThemeOptions = {
  palette: {
    mode: 'dark',
    primary: { main: '#00bcd4' },
    secondary: { main: '#7c4dff' },
  },
  shape: { borderRadius: 4 },
};

// Compose: base + tenant overrides via deepmerge
export function createTenantTheme(overrides: ThemeOptions) {
  const merged = deepmerge(baseThemeOptions, overrides);
  return createTheme(merged);
}

// Usage
const acmeTheme = createTenantTheme(acmeOverrides);
const globexTheme = createTenantTheme(globexOverrides);
```

---

## Tenant-Specific Branding

A token-based approach: load brand colors, fonts, logos, and favicon from a config
object. This separates brand identity from theme mechanics.

```tsx
import { createTheme, type Theme, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';

// Brand tokens — everything that varies per tenant
export interface BrandTokens {
  id: string;
  name: string;
  primaryColor: string;
  secondaryColor: string;
  errorColor?: string;
  warningColor?: string;
  successColor?: string;
  fontFamily: string;
  headingFontFamily?: string;
  borderRadius: number;
  logoUrl: string;
  logoHeight: number;         // px
  faviconUrl: string;
  mode: 'light' | 'dark';
  customShadows?: boolean;    // use flat design (no shadows)
}

const baseThemeOptions: ThemeOptions = {
  typography: {
    button: { textTransform: 'none' },
  },
  components: {
    MuiButton: { defaultProps: { disableElevation: true } },
    MuiTextField: { defaultProps: { variant: 'outlined', size: 'small' } },
  },
};

export function createBrandTheme(tokens: BrandTokens): Theme {
  const brandOverrides: ThemeOptions = {
    palette: {
      mode: tokens.mode,
      primary: { main: tokens.primaryColor },
      secondary: { main: tokens.secondaryColor },
      ...(tokens.errorColor && { error: { main: tokens.errorColor } }),
      ...(tokens.warningColor && { warning: { main: tokens.warningColor } }),
      ...(tokens.successColor && { success: { main: tokens.successColor } }),
    },
    typography: {
      fontFamily: tokens.fontFamily,
      h1: { fontFamily: tokens.headingF

Related in General