Theme
Coreola ships two themes — light and dark — built on a shared token base. This page covers how the theme is assembled, how to tune it for your brand, and how to add or replace a theme.
The design-language reference (when to use which color, how spacing pairs with type) lives in the Design System section. This page is the how to change it companion.
- Source:
src/themes/ - Switch:
src/components/themeSwitch/ThemeSwitch.tsx - State:
src/slices/app/app.slice.ts(getCurrentTheme,setCurrentTheme)
File map
src/themes/
├─ themes.ts # Theme factory exporting { light, dark }
├─ interface.ts # Type extensions on MUI's Theme interface
├─ defaults/
│ ├─ colors.ts # Raw color scales (violet, garden, sand, ...)
│ ├─ options.ts # Shared MUI theme options (spacing, components, typography, breakpoints, shape)
│ ├─ componentsOwn.ts # Coreola-specific MUI component variants
│ └─ helpers.ts # rem(), getTransition(), spacingNumber()
├─ light/
│ ├─ palette.ts # Light-mode palette (mode: 'light', primary, secondary, ...)
│ ├─ components.ts # Light-mode-only component overrides
│ └─ shadows.ts # Light elevation scale
└─ dark/
├─ palette.ts # Dark-mode palette
├─ components.ts # Dark-mode-only component overrides
└─ shadows.ts # Dark elevation scalethemes.ts composes each mode by merging defaults/options.ts with the mode-specific palette, components, and shadows. The result is two Theme objects MUI can consume.
How a theme is switched at runtime
- The
ThemeSwitchcomponent dispatchessetCurrentTheme({ alias: 'dark' }). - The
appslice updates which entry inapp.themeshascurrent: true. getCurrentThemeselector returns the active alias.App.tsxreads the alias and feeds the correspondingThemeobject into MUI’s<ThemeProvider>.- The whole tree re-renders against the new theme.
The active alias is persisted in the app slice (see State Management), so the user’s choice survives reloads.
Customizing palette colors
The fastest way to rebrand Coreola is to change the scales in src/themes/defaults/colors.ts. The palette mappings in light/palette.ts and dark/palette.ts reference scales by name — so swapping the underlying values updates everything coherently.
Example: change the primary brand color
The primary brand is violet. To switch to a different hue, change the values in the violet scale:
// src/themes/defaults/colors.ts
export const violet = {
50: '#...', 100: '#...', 200: '#...', 300: '#...', 400: '#...',
500: '#...', // ← main
600: '#...', 700: '#...', 800: '#...', 900: '#...',
};Generate the ramp from your brand color with a tool like Material Design 3 color builder , then paste the values in. The palette mapping still says primary: { main: violet[500] } — it now resolves to the new color.
If you want to rename violet itself, also update:
light/palette.tsanddark/palette.ts(the role mappings).- Any direct references to
violetin component overrides (options.ts).
Adding a new scale
For a new tonal color (e.g., a “teal” accent), follow Colors → Adding a new color.
Tuning component defaults
The components block in src/themes/defaults/options.ts is where MUI components are styled at a single point. It is already extensive in Coreola — most MUI primitives have entries.
To tune a component:
- Find the relevant
Mui*entry inoptions.ts. - Edit
defaultProps,styleOverrides, orvariants. - Save — Vite hot-reloads the theme; every instance of the component picks up the change.
Light-mode-only or dark-mode-only changes
defaults/options.ts is mode-agnostic. For mode-specific overrides, edit:
light/components.ts— applied only in light mode.dark/components.ts— applied only in dark mode.
A common case: the MuiPaper border looks fine in light but needs a slightly different rule in dark. Put the dark variant in dark/components.ts, leave the rest alone.
Tuning typography
The base font family, weights, and button overrides live in defaults/options.ts → typography. The base font size is in src/app/settings.ts (baseFontSize: 13).
To swap the typeface:
- Install a font package (Coreola uses
@fontsource/*). - Import it once at the app root.
- Update
typography.fontFamilyinoptions.tsto put your font first.
The full type-scale story is in Typography.
Tuning spacing and shape
- Spacing unit —
options.ts → spacing(default4). Bumping this to8is a one-line change that doubles every gap in the app. Do not do that lightly; the rest of the design assumes4. - Corner radii —
options.ts → shape(borderRadius,borderRadiusMedium,borderRadiusLarge). - Breakpoints —
options.ts → breakpoints.values. Coreola uses MUI defaults exceptxl: 1440(down from MUI’s 1536).
See Spacing for the spacing model in full.
Adding a new theme
To ship a third theme (high-contrast, branded variant, customer-specific):
- Create a folder under
src/themes/(e.g.,highContrast/) withpalette.ts,components.ts,shadows.ts. - Register it in
themes.tsby composing the same factory used for light/dark. - Add it to the theme list in
src/app/settings.ts(themesblock) — each theme has analias,title, optionalcurrent, and optionaldisabledfields. - The
ThemeSwitchUI reads the theme list from theappslice and renders it automatically — you do not need to change the switch.
Removing a theme
Set disabled: true in the theme’s entry in settings.ts. Selectors filter disabled themes out. The theme files can stay if you might reactivate later.
Per-user vs per-environment themes
Theme is per-user. It is stored in the app slice (persisted to localStorage), not per-environment. This is by design — admin users on the same product can prefer different modes.
If you need a per-environment theme (a customer-specific build), branch at the factory:
// themes.ts
const brand = process.env.VITE_APP_BRAND ?? 'default';
export const themes = brand === 'acme' ? acmeThemes : defaultThemes;Then set VITE_APP_BRAND in .env.local (see Environment).
Using the theme inside components
Three preferred patterns:
sx with palette tokens
<Box sx={{ color: 'text.secondary', borderColor: 'border.light', p: 2 }} />The cleanest. The token strings resolve against the active theme.
useTheme() for dynamic values
import { useTheme } from '@mui/material/styles';
const theme = useTheme();
const chartFill = theme.palette.color.chart[400];When you need a specific scale step or pass a value to a non-MUI library (charts).
styled() with the theme callback
const Wrap = styled('div')(({ theme }) => ({
padding: theme.spacing(3),
background: theme.palette.background.paper,
}));For component-level styles that compose multiple tokens.
Forbidden — raw values
sx={{ color: '#693F83' }} // ❌Raw colors do not respond to theme switches and break dark mode.
Theme type augmentation
src/themes/interface.ts extends MUI’s Theme and Palette interfaces with Coreola-specific fields (palette.color.chart, shape.borderRadiusMedium, etc.). This is how TypeScript knows about the custom tokens.
If you add a new field to the palette or shape, also extend the interface so the types stay aligned. Without this, theme.shape.myNew is a type error even when it works at runtime.
Anti-patterns
- Per-component theme overrides scattered in component files. Put overrides in
defaults/options.ts(or mode-specificcomponents.ts). One source of truth. - Raw hex colors in component code. Always go through the palette.
- Changing
baseFontSizewithout auditing. The component overrides inoptions.tsassume 13. A change here may require adjusting padding constants. - A new theme that is 90% the same as light. Use
disabled: falsetoggles, not a near-duplicate theme. - Forgetting to extend
interface.tswhen adding palette/shape fields. Compiler errors will follow.
Next steps
- Colors — palette and color roles
- Typography — type scale
- Spacing — spacing grid + breakpoints + shape
- State Management — how
setCurrentThemelives in theappslice