7.7 KiB
title, tags, created
| title | tags | created | |||
|---|---|---|---|---|---|
| Theme Configuration |
|
2026-05-23 |
Theme Configuration
The MUI theme is constructed in frontend/src/theme/index.ts and composed from option modules in frontend/src/theme/options/. The resulting theme is provided at the root layout, wrapped by an RTL-aware emotion cache.
1. Construction pipeline
// approximate — read theme/index.ts for the canonical version
import { createTheme } from '@mui/material/styles';
import { palette } from './options/palette';
import { typography } from './options/typography';
import { shadows, customShadows } from './options/shadows';
import { componentsOverrides } from './options/overrides';
export function buildTheme(opts: { mode: 'light' | 'dark'; direction: 'ltr' | 'rtl'; preset: string; }) {
return createTheme({
direction: opts.direction,
palette: palette(opts.mode, opts.preset),
typography,
shape: { borderRadius: 8 },
shadows: shadows(opts.mode),
customShadows: customShadows(opts.mode),
components: componentsOverrides(opts),
breakpoints: {
values: { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 },
},
});
}
The settings context calls buildTheme() whenever any axis changes, then re-mounts <ThemeProvider>. Because MUI v7 supports CSS variables, the swap is cheap (no React tree thrash).
2. Provider wiring (root layout)
// frontend/src/app/layout.tsx (simplified)
<AppRouterCacheProvider options={{ key: dir === 'rtl' ? 'css-rtl' : 'css', prepend: true, stylisPlugins: dir === 'rtl' ? [rtlPlugin] : [] }}>
<ThemeProvider theme={theme}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs}>
<I18nProvider>
<QueryClientProvider client={queryClient}>
<SocketProvider>
<SnackbarProvider>{children}</SnackbarProvider>
</SocketProvider>
</QueryClientProvider>
</I18nProvider>
</LocalizationProvider>
</ThemeProvider>
</AppRouterCacheProvider>
Warning
The cache
keyMUST differ between LTR and RTL ('css'vs'css-rtl') — otherwise switching direction at runtime keeps the previous direction's CSS in the head and the layout breaks.
3. Palette
See Colors for the full table. Structure:
{
mode: 'light' | 'dark',
primary: { lighter, light, main, dark, darker, contrastText },
secondary: { lighter, light, main, dark, darker, contrastText },
info: { lighter, light, main, dark, darker, contrastText },
success: { lighter, light, main, dark, darker, contrastText },
warning: { lighter, light, main, dark, darker, contrastText },
error: { lighter, light, main, dark, darker, contrastText },
grey: { 100…900 },
text: { primary, secondary, disabled },
background:{ default, paper, neutral },
action: { active, hover, selected, focus, disabled, disabledBackground },
}
Color presets (selectable in settings) swap the primary + secondary color groups while leaving grey, error, etc. untouched.
4. Typography
{
fontFamily: '"Public Sans Variable", "Helvetica", "Arial", sans-serif',
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightSemiBold: 600,
fontWeightBold: 700,
h1: { fontSize: 64, lineHeight: 80/64, letterSpacing: -1 },
h2: { fontSize: 48, lineHeight: 64/48 },
h3: { fontSize: 32, lineHeight: 48/32 },
h4: { fontSize: 24, lineHeight: 36/24 },
h5: { fontSize: 20, lineHeight: 30/20 },
h6: { fontSize: 18, lineHeight: 28/18 },
subtitle1: { fontSize: 16, fontWeight: 600 },
subtitle2: { fontSize: 14, fontWeight: 600 },
body1: { fontSize: 16 },
body2: { fontSize: 14 },
caption: { fontSize: 12 },
overline: { fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1.1 },
button: { fontSize: 14, fontWeight: 700, textTransform: 'unset' },
}
Variants h1 and h2 scale down responsively. Use the responsiveFontSizes(theme) helper if you need automatic scaling.
See Typography for font loading, secondary font (Barlow), and i18n font fallbacks.
5. Spacing
spacing: 8 // unit
// theme.spacing(1) === '8px'
// theme.spacing(2) === '16px'
// theme.spacing(0.5) === '4px'
Use the shorthand props on the sx:
<Box sx={{ p: 2, mt: 3, mx: { xs: 1, md: 4 } }} />
6. Shape & radii
shape: { borderRadius: 8 } // default
// cards typically: borderRadius: 16
// pills: borderRadius: 9999
7. Shadows
MUI's default shadows array (25 levels: none, then 1–24) is replaced with a softer custom set. Additionally, customShadows defines brand-specific tinted shadows used by the Card, Button, Dialog overrides:
customShadows: {
z1, z4, z8, z12, z16, z20, z24,
primary: 'rgba(<primary.main>, 0.24) 0 8 16 0',
secondary: '…',
info: '…',
success: '…',
warning: '…',
error: '…',
card: 'rgba(0,0,0,0.04) 0 0 2 0, rgba(0,0,0,0.08) 0 12 24 -4',
dialog: '…',
dropdown: '…',
}
Read inside sx:
sx={{ boxShadow: (theme) => theme.customShadows.z8 }}
8. Component overrides
theme/options/overrides/ contains one file per overridden component, each exporting { defaultProps, styleOverrides } merged into components:
| Component | Notable override |
|---|---|
MuiButton |
disableRipple, custom customShadows.primary on contained variant |
MuiCard |
borderRadius: 16, customShadows.card |
MuiPaper |
elevation flattened to 0 by default (use card shadow explicitly) |
MuiTextField |
variant: 'outlined' default, custom focus ring |
MuiTooltip |
dark background regardless of mode, smaller padding |
MuiAvatar |
letter-based fallback with hashed background color |
MuiTableCell |
denser padding, header weight 600 |
MuiDialog |
customShadows.dialog, max-width responsive |
MuiAlert |
tinted background using palette lighter |
MuiTab |
uppercase off, color from primary.main when selected |
MuiAutocomplete |
custom popup paper shadow |
Add an override:
- Create
theme/options/overrides/<Component>.tsexporting a function(theme) => ({...}). - Register it in
theme/options/overrides/index.ts. - It will be merged into the next
createThemecall automatically.
9. CSS variables (MUI v7)
createTheme with cssVariables: true (recommended) emits CSS custom properties at the <html> root and switches them per mode without re-rendering. The default is on in v7 — verify in theme/index.ts.
This means a custom CSS rule can reference:
.my-thing {
background: var(--mui-palette-primary-main);
padding: calc(var(--mui-spacing) * 2);
}
Use this sparingly — prefer the sx prop.
10. Per-locale font swap
When the active locale is fa or ar, the typography option module can layer a Persian/Arabic font (e.g., Vazirmatn, IRANSans) ahead of Public Sans Variable:
fontFamily: dir === 'rtl'
? '"Vazirmatn", "Public Sans Variable", sans-serif'
: '"Public Sans Variable", sans-serif',
Otherwise Latin glyphs still render via the Public Sans fallback. See Internationalization & RTL.
11. Customisation checklist
- New brand color → update
palette.ts(light + dark) and consider a preset. - New variant → add to typography options and the
TypographyOptionsinterface augmentation. - Component override → file in
overrides/. - Verify in both modes (light + dark) AND both directions (ltr + rtl).
- Verify against WCAG AA contrast.
- Snapshot test the component if visual stability matters.