Files
nick-doc/05 - Design System/Theme Configuration.md
2026-05-23 20:35:34 +03:30

7.7 KiB
Raw Blame History

title, tags, created
title tags created
Theme Configuration
design-system
theme
mui
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 key MUST 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:      { 100900 },
  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 124) 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:

  1. Create theme/options/overrides/<Component>.ts exporting a function (theme) => ({...}).
  2. Register it in theme/options/overrides/index.ts.
  3. It will be merged into the next createTheme call 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 TypographyOptions interface 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.