---
name: white-label
description: MUI multi-theme and white-label systems — nested ThemeProvider, dynamic theme switching, tenant-specific branding, and theme composition
triggers:
- white-label
- multi-theme
- multi-tenant
- nested ThemeProvider
- dynamic theme
- tenant branding
- theme switching
allowed-tools:
- Read
- Glob
- Grep
- Write
- Edit
globs:
- "*.tsx"
- "*.ts"
- "theme*.ts"
---
# 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 (
{/* Dark sidebar — scoped theme */}
Admin
{/* Main content — inherits mainTheme */}
Welcome
);
}
```
### Nested Theme with Callback (Merge Instead of Replace)
Pass a function to `ThemeProvider` to receive the outer theme and merge selectively:
```tsx
createTheme({
...outerTheme,
palette: {
...outerTheme.palette,
primary: { main: '#e91e63' }, // override only primary
},
})
}
>
```
---
## 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 {
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(null);
const [error, setError] = useState(null);
useEffect(() => {
fetchBrandConfig(tenantId)
.then(setBrandConfig)
.catch((err) => setError(err.message));
}, [tenantId]);
const theme = useMemo(
() => (brandConfig ? createTheme(buildThemeOptions(brandConfig)) : null),
[brandConfig],
);
if (error) return Theme load failed: {error};
if (!theme) {
return (
);
}
return (
{children}
);
}
```
---
## 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.headingFontFamily ?? tokens.fontFamily },
h2: { fontFamily: tokens.headingFontFamily ?? tokens.fontFamily },
h3: { fontFamily: tokens.headingFontFamily ?? tokens.fontFamily },
},
shape: { borderRadius: tokens.borderRadius },
...(tokens.customShadows === false && {
shadows: Array(25).fill('none') as Theme['shadows'],
}),
};
return createTheme(deepmerge(baseThemeOptions, brandOverrides));
}
// React context to expose brand tokens (logo, favicon, etc.) outside the theme
import { createContext, useContext, type ReactNode } from 'react';
const BrandContext = createContext(null);
export function useBrand(): BrandTokens {
const brand = useContext(BrandContext);
if (!brand) throw new Error('useBrand must be used within BrandProvider');
return brand;
}
export function BrandProvider({ tokens, children }: { tokens: BrandTokens; children: ReactNode }) {
return {children};
}
```
Usage in a header component:
```tsx
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Box from '@mui/material/Box';
import { useBrand } from './brand';
export function BrandedHeader() {
const brand = useBrand();
return (
);
}
```
---
## CSS Variables Approach
MUI v5.1+ supports `CssVarsProvider` which uses CSS custom properties for palette
values. This enables theme switching without React re-renders — the browser just
swaps variable values.
```tsx
import {
Experimental_CssVarsProvider as CssVarsProvider,
experimental_extendTheme as extendTheme,
useColorScheme,
} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import type { ReactNode } from 'react';
// extendTheme replaces createTheme when using CssVarsProvider
const theme = extendTheme({
colorSchemes: {
light: {
palette: {
primary: { main: '#1976d2' },
background: { default: '#fafafa', paper: '#ffffff' },
},
},
dark: {
palette: {
primary: { main: '#90caf9' },
background: { default: '#121212', paper: '#1e1e1e' },
},
},
},
typography: {
fontFamily: '"Inter", sans-serif',
button: { textTransform: 'none' },
},
});
// Toggle component — switches mode via CSS variables, zero re-render
function ModeToggle() {
const { mode, setMode } = useColorScheme();
return (
setMode(mode === 'light' ? 'dark' : 'light')}
aria-label={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}
>
{mode === 'light' ? : }
);
}
export function CssVarsApp({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
### Accessing CSS Variables in sx
```tsx
```
### CssVarsProvider with Multi-Tenant
Combine `extendTheme` with tenant overrides:
```tsx
import { experimental_extendTheme as extendTheme } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
const baseExtended = {
colorSchemes: {
light: { palette: { primary: { main: '#1976d2' } } },
dark: { palette: { primary: { main: '#90caf9' } } },
},
};
function createTenantCssVarsTheme(overrides: Record) {
return extendTheme(deepmerge(baseExtended, overrides));
}
// Tenant override — only changes primary color for both schemes
const acmeCssVarsTheme = createTenantCssVarsTheme({
colorSchemes: {
light: { palette: { primary: { main: '#ff5722' } } },
dark: { palette: { primary: { main: '#ff8a65' } } },
},
});
```
---
## Theme Registry Pattern
Map tenant IDs to theme objects. Useful when you have a known set of tenants at
build time, or when you cache API-loaded themes.
```tsx
import { createTheme, type Theme, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
const baseOptions: ThemeOptions = {
typography: { button: { textTransform: 'none' } },
shape: { borderRadius: 8 },
};
// Registry: tenant ID -> theme options (only the diff from base)
const tenantThemeOverrides: Record = {
acme: {
palette: { primary: { main: '#ff5722' }, secondary: { main: '#ff9800' } },
typography: { fontFamily: '"Poppins", sans-serif' },
},
globex: {
palette: { mode: 'dark', primary: { main: '#00bcd4' } },
shape: { borderRadius: 4 },
},
initech: {
palette: { primary: { main: '#4caf50' }, secondary: { main: '#8bc34a' } },
typography: { fontFamily: '"Roboto Mono", monospace' },
},
};
// Cache to avoid re-creating themes on every render
const themeCache = new Map();
export function getTenantTheme(tenantId: string): Theme {
const cached = themeCache.get(tenantId);
if (cached) return cached;
const overrides = tenantThemeOverrides[tenantId];
if (!overrides) {
console.warn(`No theme registered for tenant "${tenantId}", using base theme`);
const fallback = createTheme(baseOptions);
themeCache.set(tenantId, fallback);
return fallback;
}
const theme = createTheme(deepmerge(baseOptions, overrides));
themeCache.set(tenantId, theme);
return theme;
}
// Dynamic registration for API-loaded tenants
export function registerTenantTheme(tenantId: string, overrides: ThemeOptions): void {
tenantThemeOverrides[tenantId] = overrides;
themeCache.delete(tenantId); // invalidate cache
}
```
Usage:
```tsx
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { getTenantTheme } from './theme-registry';
function App({ tenantId }: { tenantId: string }) {
const theme = getTenantTheme(tenantId);
return (
{/* app content */}
);
}
```
---
## Component Variant Overrides per Tenant
Tenants may need different component appearances beyond colors — pill buttons for
one brand, square for another, outlined defaults for a third.
```tsx
import { createTheme, type ThemeOptions, type Components, type Theme } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
// Each tenant defines component overrides
type TenantComponents = Components>;
const acmeComponents: TenantComponents = {
MuiButton: {
defaultProps: { variant: 'contained', disableElevation: true },
styleOverrides: {
root: { borderRadius: 24, padding: '10px 28px', fontWeight: 700 },
containedPrimary: {
background: 'linear-gradient(45deg, #ff5722, #ff9800)',
'&:hover': { background: 'linear-gradient(45deg, #e64a19, #f57c00)' },
},
},
},
MuiCard: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: {
borderRadius: 16,
border: '2px solid',
borderColor: 'rgba(255, 87, 34, 0.2)',
},
},
},
MuiChip: {
defaultProps: { variant: 'filled' },
styleOverrides: {
root: { borderRadius: 24, fontWeight: 600 },
},
},
};
const initechComponents: TenantComponents = {
MuiButton: {
defaultProps: { variant: 'outlined' },
styleOverrides: {
root: { borderRadius: 0, textTransform: 'uppercase', letterSpacing: 1 },
},
},
MuiCard: {
defaultProps: { elevation: 2 },
styleOverrides: {
root: { borderRadius: 0 },
},
},
};
// Compose with base theme
const baseOptions: ThemeOptions = {
typography: { button: { textTransform: 'none' } },
};
export const acmeTheme = createTheme(
deepmerge(baseOptions, {
palette: { primary: { main: '#ff5722' } },
components: acmeComponents,
}),
);
export const initechTheme = createTheme(
deepmerge(baseOptions, {
palette: { primary: { main: '#4caf50' } },
components: initechComponents,
}),
);
```
---
## TypeScript for Multi-Theme
### Generic Theme Factory Function
```tsx
import { createTheme, type Theme, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
// Shared base type for all tenant configs
interface TenantThemeConfig {
tenantId: string;
palette: {
primary: string;
secondary: string;
mode?: 'light' | 'dark';
};
typography?: {
fontFamily?: string;
headingFontFamily?: string;
};
shape?: {
borderRadius?: number;
};
components?: ThemeOptions['components'];
}
const baseThemeOptions: ThemeOptions = {
typography: {
fontFamily: '"Inter", sans-serif',
button: { textTransform: 'none' },
},
shape: { borderRadius: 8 },
components: {
MuiButton: { defaultProps: { disableElevation: true } },
MuiTextField: { defaultProps: { variant: 'outlined', size: 'small' } },
},
};
export function createThemeFromConfig(config: TenantThemeConfig): Theme {
const overrides: ThemeOptions = {
palette: {
mode: config.palette.mode ?? 'light',
primary: { main: config.palette.primary },
secondary: { main: config.palette.secondary },
},
...(config.typography && {
typography: {
fontFamily: config.typography.fontFamily,
...(config.typography.headingFontFamily && {
h1: { fontFamily: config.typography.headingFontFamily },
h2: { fontFamily: config.typography.headingFontFamily },
h3: { fontFamily: config.typography.headingFontFamily },
h4: { fontFamily: config.typography.headingFontFamily },
}),
},
}),
...(config.shape && { shape: config.shape }),
...(config.components && { components: config.components }),
};
return createTheme(deepmerge(baseThemeOptions, overrides));
}
// Type-safe tenant registry
const configs: Record = {
acme: {
tenantId: 'acme',
palette: { primary: '#ff5722', secondary: '#ff9800' },
typography: { fontFamily: '"Poppins", sans-serif' },
},
globex: {
tenantId: 'globex',
palette: { primary: '#00bcd4', secondary: '#7c4dff', mode: 'dark' },
shape: { borderRadius: 4 },
},
};
export function getThemeForTenant(tenantId: string): Theme {
const config = configs[tenantId];
if (!config) throw new Error(`Unknown tenant: ${tenantId}`);
return createThemeFromConfig(config);
}
```
### Augmenting the Theme Type for Custom Tokens
```tsx
import { createTheme } from '@mui/material/styles';
// Declare custom tokens on the theme
declare module '@mui/material/styles' {
interface Theme {
brand: {
logoUrl: string;
logoHeight: number;
appName: string;
};
}
interface ThemeOptions {
brand?: {
logoUrl?: string;
logoHeight?: number;
appName?: string;
};
}
}
const acmeTheme = createTheme({
palette: { primary: { main: '#ff5722' } },
brand: {
logoUrl: '/brands/acme/logo.svg',
logoHeight: 40,
appName: 'Acme Portal',
},
});
// Now type-safe in any component:
// const theme = useTheme();
// theme.brand.logoUrl <-- autocompletes
```
---
## Performance
### Memoize Theme Creation
`createTheme` is expensive. Never call it inside a render function without memoization.
```tsx
import { useMemo } from 'react';
import { createTheme, ThemeProvider, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
const baseOptions: ThemeOptions = { /* ... */ };
function TenantApp({ overrides, children }: { overrides: ThemeOptions; children: React.ReactNode }) {
// GOOD — only re-creates when overrides reference changes
const theme = useMemo(
() => createTheme(deepmerge(baseOptions, overrides)),
[overrides],
);
return {children};
}
```
### Avoid Re-Creating Overrides Object
```tsx
// BAD — new object on every render, theme re-created every time
function App({ primaryColor }: { primaryColor: string }) {
return (
{/* ... */}
);
}
// GOOD — stable reference
function App({ primaryColor }: { primaryColor: string }) {
const overrides = useMemo(
() => ({ palette: { primary: { main: primaryColor } } }),
[primaryColor],
);
return {/* ... */};
}
```
### Cache Themes for Known Tenants
See the Theme Registry Pattern section above. The `Map`-based cache prevents
re-creating the same theme on navigation or re-mount.
---
## Complete White-Label Architecture
Full example tying together theme factory, tenant loader, brand context, and provider.
### File: `lib/brand/types.ts`
```tsx
export interface TenantBrandConfig {
tenantId: string;
name: string;
domain: string;
logoUrl: string;
logoHeight: number;
faviconUrl: string;
palette: {
mode: 'light' | 'dark';
primary: string;
secondary: string;
error?: string;
warning?: string;
success?: string;
};
typography: {
fontFamily: string;
headingFontFamily?: string;
googleFontsUrl?: string; // link to Google Fonts CSS
};
shape: {
borderRadius: number;
};
components?: {
buttonVariant?: 'contained' | 'outlined' | 'text';
buttonBorderRadius?: number;
cardElevation?: number;
cardBorderRadius?: number;
};
}
```
### File: `lib/brand/theme-factory.ts`
```tsx
import { createTheme, type Theme, type ThemeOptions } from '@mui/material/styles';
import deepmerge from '@mui/utils/deepmerge';
import type { TenantBrandConfig } from './types';
const BASE_THEME: ThemeOptions = {
typography: {
button: { textTransform: 'none', fontWeight: 600 },
h1: { fontWeight: 800 },
h2: { fontWeight: 700 },
},
components: {
MuiButton: { defaultProps: { disableElevation: true } },
MuiTextField: { defaultProps: { variant: 'outlined', size: 'small' } },
MuiCssBaseline: {
styleOverrides: {
body: { scrollBehavior: 'smooth' },
},
},
},
};
const cache = new Map();
export function createBrandTheme(config: TenantBrandConfig): Theme {
const cached = cache.get(config.tenantId);
if (cached) return cached;
const overrides: ThemeOptions = {
palette: {
mode: config.palette.mode,
primary: { main: config.palette.primary },
secondary: { main: config.palette.secondary },
...(config.palette.error && { error: { main: config.palette.error } }),
...(config.palette.warning && { warning: { main: config.palette.warning } }),
...(config.palette.success && { success: { main: config.palette.success } }),
},
typography: {
fontFamily: config.typography.fontFamily,
...(config.typography.headingFontFamily && {
h1: { fontFamily: config.typography.headingFontFamily },
h2: { fontFamily: config.typography.headingFontFamily },
h3: { fontFamily: config.typography.headingFontFamily },
h4: { fontFamily: config.typography.headingFontFamily },
}),
},
shape: { borderRadius: config.shape.borderRadius },
components: {
...(config.components?.buttonVariant && {
MuiButton: {
defaultProps: { variant: config.components.buttonVariant },
...(config.components.buttonBorderRadius != null && {
styleOverrides: {
root: { borderRadius: config.components.buttonBorderRadius },
},
}),
},
}),
...(config.components?.cardElevation != null && {
MuiCard: {
defaultProps: { elevation: config.components.cardElevation },
...(config.components.cardBorderRadius != null && {
styleOverrides: {
root: { borderRadius: config.components.cardBorderRadius },
},
}),
},
}),
},
};
const theme = createTheme(deepmerge(BASE_THEME, overrides));
cache.set(config.tenantId, theme);
return theme;
}
export function invalidateBrandCache(tenantId?: string): void {
if (tenantId) {
cache.delete(tenantId);
} else {
cache.clear();
}
}
```
### File: `lib/brand/BrandProvider.tsx`
```tsx
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import {
createContext,
useContext,
useState,
useEffect,
useMemo,
type ReactNode,
} from 'react';
import type { TenantBrandConfig } from './types';
import { createBrandTheme } from './theme-factory';
// --- Brand Context ---
const BrandContext = createContext(null);
export function useBrand(): TenantBrandConfig {
const ctx = useContext(BrandContext);
if (!ctx) throw new Error('useBrand() must be used within ');
return ctx;
}
// --- Tenant Loader ---
async function loadTenantConfig(tenantId: string): Promise {
const res = await fetch(`/api/tenants/${tenantId}/brand`);
if (!res.ok) throw new Error(`Brand config fetch failed (${res.status})`);
return res.json();
}
// --- Font Loader ---
function loadGoogleFonts(url: string): void {
if (document.querySelector(`link[href="${url}"]`)) return;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
}
// --- Favicon Setter ---
function setFavicon(url: string): void {
let link = document.querySelector('link[rel="icon"]');
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = url;
}
// --- Provider Component ---
interface BrandProviderProps {
tenantId: string;
fallbackConfig?: TenantBrandConfig; // optional static fallback
children: ReactNode;
}
export function BrandProvider({ tenantId, fallbackConfig, children }: BrandProviderProps) {
const [config, setConfig] = useState(fallbackConfig ?? null);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
loadTenantConfig(tenantId)
.then((cfg) => {
if (cancelled) return;
setConfig(cfg);
// Side effects: load fonts, set favicon, set document title
if (cfg.typography.googleFontsUrl) loadGoogleFonts(cfg.typography.googleFontsUrl);
if (cfg.faviconUrl) setFavicon(cfg.faviconUrl);
document.title = cfg.name;
})
.catch((err) => {
if (!cancelled) setError(err.message);
});
return () => { cancelled = true; };
}, [tenantId]);
const theme = useMemo(() => (config ? createBrandTheme(config) : null), [config]);
if (error && !config) {
return (
Failed to load brand: {error}
);
}
if (!theme || !config) {
return (
);
}
return (
{children}
);
}
```
### File: `App.tsx` (entry point)
```tsx
import { BrandProvider } from './lib/brand/BrandProvider';
import { BrandedHeader } from './components/BrandedHeader';
import { Dashboard } from './pages/Dashboard';
// Tenant ID typically comes from subdomain, URL param, or env variable
function getTenantId(): string {
const subdomain = window.location.hostname.split('.')[0];
return subdomain === 'localhost' ? 'acme' : subdomain;
}
export default function App() {
const tenantId = getTenantId();
return (
);
}
```
### File: `components/BrandedHeader.tsx`
```tsx
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { useBrand } from '../lib/brand/BrandProvider';
export function BrandedHeader() {
const brand = useBrand();
return (
{brand.name}
);
}
```