This document describes the theming system in the Rubrion Web Client Template, which is built on top of Material UI's theming capabilities.
The theming system provides:
- Light and dark mode support
- Customizable color palettes
- Typography scale
- Responsive breakpoints
- Component style overrides
- Custom theme extensions
The theme is defined in the src/styles/theme.ts file and follows Material UI's theme structure with custom extensions.
The application is wrapped with a custom ThemeProvider that manages theme state:
// In App.tsx
<ThemeProvider>
<div className="app-container">{/* Application content */}</div>
</ThemeProvider>The base theme defines common properties shared across light and dark modes:
const baseTheme = {
typography: {
fontFamily: "'Inter', 'Roboto', 'Helvetica', 'Arial', sans-serif",
h1: {
fontSize: '2.5rem',
fontWeight: 700,
lineHeight: 1.2,
},
// Other typography variants...
},
shape: {
borderRadius: 8,
},
shadows: [...customShadows],
// Custom properties
custom: {
contentWidth: {
max: '1200px',
wide: '1600px',
},
// Other custom properties...
},
};Separate theme objects for light and dark modes:
const lightTheme = createTheme(
deepmerge(baseTheme, {
palette: {
mode: 'light',
primary: {
main: '#2563eb',
light: '#93c5fd',
dark: '#1d4ed8',
contrastText: '#ffffff',
},
secondary: {
main: '#8b5cf6',
light: '#c4b5fd',
dark: '#7c3aed',
contrastText: '#ffffff',
},
background: {
default: '#f8fafc',
paper: '#ffffff',
},
text: {
primary: '#1e293b',
secondary: '#64748b',
disabled: '#94a3b8',
},
// Other color definitions...
},
// Other light mode specific overrides...
})
);
const darkTheme = createTheme(
deepmerge(baseTheme, {
palette: {
mode: 'dark',
primary: {
main: '#3b82f6',
light: '#60a5fa',
dark: '#2563eb',
contrastText: '#ffffff',
},
secondary: {
main: '#a78bfa',
light: '#c4b5fd',
dark: '#7c3aed',
contrastText: '#ffffff',
},
background: {
default: '#0f172a',
paper: '#1e293b',
},
text: {
primary: '#f1f5f9',
secondary: '#cbd5e1',
disabled: '#64748b',
},
// Other color definitions...
},
// Other dark mode specific overrides...
})
);The theme includes global component style overrides:
// Example component style overrides
const components = {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: '8px',
padding: '10px 24px',
fontWeight: 600,
},
contained: ({ theme }) => ({
boxShadow: 'none',
'&:hover': {
boxShadow: theme.shadows[1],
},
}),
// Other button variants...
},
variants: [
{
props: { variant: 'gradient' },
style: ({ theme }) => ({
background: `linear-gradient(45deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
color: theme.palette.primary.contrastText,
}),
},
],
},
// Other component overrides...
};These overrides are applied to the theme:
// Apply component overrides to both themes
lightTheme.components = deepmerge(lightTheme.components || {}, components);
darkTheme.components = deepmerge(darkTheme.components || {}, components);Access the theme and theme mode using the useTheme hook:
import { useTheme } from '../hooks/useTheme';
const MyComponent = () => {
const { theme, themeMode, toggleTheme } = useTheme();
return (
<div>
<Button
onClick={toggleTheme}
startIcon={themeMode === 'dark' ? <LightModeIcon /> : <DarkModeIcon />}
>
Switch to {themeMode === 'dark' ? 'Light' : 'Dark'} Mode
</Button>
<Box
sx={{
bgcolor: theme.palette.background.default,
color: theme.palette.text.primary,
}}
>
Content with theme-aware colors
</Box>
</div>
);
};Use the theme in styled components:
import { styled } from '@mui/material/styles';
const StyledCard = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(3),
boxShadow: theme.shadows[1],
color: theme.palette.text.primary,
'&:hover': {
boxShadow: theme.shadows[3],
},
[theme.breakpoints.down('sm')]: {
padding: theme.spacing(2),
},
}));
// Usage
const MyComponent = () => {
return <StyledCard>Card content</StyledCard>;
};Use the theme with the sx prop:
import { Box, Typography } from '@mui/material';
const MyComponent = () => {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
p: { xs: 2, md: 4 },
bgcolor: 'background.paper',
borderRadius: 2,
}}
>
<Typography
variant="h4"
sx={{
color: 'primary.main',
fontWeight: 'bold',
textAlign: 'center',
}}
>
Title
</Typography>
</Box>
);
};The theme includes responsive breakpoints for different screen sizes:
// Access breakpoints in your components
import { useTheme } from '@mui/material/styles';
const MyComponent = () => {
const theme = useTheme();
return (
<Box
sx={{
[theme.breakpoints.up('md')]: {
display: 'flex',
},
[theme.breakpoints.down('sm')]: {
display: 'block',
},
}}
>
{/* Content */}
</Box>
);
};The theme includes custom properties for application-specific settings:
declare module '@mui/material/styles' {
interface Theme {
custom: {
contentWidth: {
max: string;
wide: string;
};
navbarHeight: {
mobile: number;
desktop: number;
};
transitions: {
duration: {
slow: number;
medium: number;
fast: number;
};
};
gradients: {
primary: string;
secondary: string;
};
cards: {
borderRadius: number;
};
};
}
interface ThemeOptions {
custom?: {
contentWidth?: {
max?: string;
wide?: string;
};
navbarHeight?: {
mobile?: number;
desktop?: number;
};
transitions?: {
duration?: {
slow?: number;
medium?: number;
fast?: number;
};
};
gradients?: {
primary?: string;
secondary?: string;
};
cards?: {
borderRadius?: number;
};
};
}
}Usage of custom theme properties:
const ContentContainer = styled('div')(({ theme }) => ({
maxWidth: theme.custom.contentWidth.max,
margin: '0 auto',
padding: theme.spacing(2),
[theme.breakpoints.up('md')]: {
padding: theme.spacing(3),
},
}));
const WideContentContainer = styled('div')(({ theme }) => ({
maxWidth: theme.custom.contentWidth.wide,
margin: '0 auto',
padding: theme.spacing(2),
}));The user's theme preference is persisted in local storage:
// In ThemeContext.tsx
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
// Check if there's a saved preference in localStorage
const savedTheme = localStorage.getItem('theme-mode');
// If there's a valid saved preference, use it
if (savedTheme === 'light' || savedTheme === 'dark') {
return savedTheme;
}
// Otherwise, use system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
});
// Update localStorage when theme changes
useEffect(() => {
localStorage.setItem('theme-mode', themeMode);
}, [themeMode]);The theme system can detect and respond to the user's system preference:
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// Update theme when system preference changes
const handleChange = (e: MediaQueryListEvent) => {
if (followSystemTheme) {
setThemeMode(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [followSystemTheme]);To prevent unnecessary re-renders, theme is memoized:
// In ThemeContext.tsx
const currentTheme = useMemo(() => {
return themeMode === 'dark' ? darkTheme : lightTheme;
}, [themeMode]);- Use Theme Colors: Always use theme palette colors instead of hardcoded values
- Responsive Design: Use theme breakpoints for responsive styling
- Custom Properties: Extend the theme with custom properties for application-specific settings
- Component Variants: Use component variants for consistent styling
- Nested Themes: You can create nested themes for specific sections of your application
- Testing: Include tests for both light and dark modes
A theme debug utility is available in development mode:
import { ThemeInspector } from '../components/debug/ThemeInspector';
// In your component
if (process.env.NODE_ENV === 'development') {
return (
<>
<Component />
<ThemeInspector />
</>
);
}The theming system provides a comprehensive solution for creating consistent, adaptive, and beautiful user interfaces. By leveraging Material UI's theming capabilities and extending them with custom properties and utilities, you can create unique designs that maintain consistency across your application.