Angular M3 Theming
In the context of the general modernization of Angular code, fully embrace Angular M3 theming and replace literal color values in components with Angular Material CSS variables. This also enables theme toggle between light and dark.
Documentation:
- https://material.angular.dev/guide/theming-your-components
- https://trailheadtechnology.com/modern-theming-in-angular-20-light-dark-mode-with-material-design-3/
- https://m3.material.io/styles/color/roles
Upgrading Styles
To upgrade Angular theming in your Cadmus editor:
(1) update packages.
(2) replace your styles.scss with this code (⚠️ be sure to avoid dropping your own imports at the top of the old file!):
@use '@angular/material' as mat;
// A theme object is still needed for mat.typography-hierarchy(), which
// requires the internal structure produced by mat.define-theme().
$_theme: mat.define-theme(
(
color: (
primary: mat.$azure-palette,
tertiary: mat.$orange-palette,
// color-scheme = use CSS light-dark() so one call covers both modes
theme-type: color-scheme,
),
typography: (
brand-family: 'Roboto',
plain-family: 'Roboto',
),
density: (
scale: 0,
),
)
);
html {
// Single mat.theme() call using theme-type: color-scheme.
// All --mat-sys-* tokens are emitted as light-dark(lightValue, darkValue).
// Light/dark mode is therefore driven entirely by the CSS color-scheme
// property below — no second mat.theme() call needed.
@include mat.theme(
(
color: (
primary: mat.$azure-palette,
tertiary: mat.$orange-palette,
theme-type: color-scheme,
),
typography: (
brand-family: 'Roboto',
plain-family: 'Roboto',
),
density: 0,
)
);
// Light by default; toggled to dark by ThemeService via .dark-mode class.
color-scheme: light;
background-color: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
&.dark-mode {
color-scheme: dark;
// No second mat.theme() needed — light-dark() resolves automatically.
}
}
// Apply Material typography scale to .mat-typography (on <body>) and to
// native h1–h6 elements contained within it.
@include mat.typography-hierarchy($_theme);
html,
body {
height: 100%;
}
body {
margin: 0;
}
// ── Semantic color utility classes ───────────────────────────────────────────
// These override the CSS custom properties that Angular Material components
// read, so they work correctly in both light and dark mode.
// mat-flat-button (M3 "filled" button)
// Note: in M3 the default filled button already uses the primary color.
// The .mat-primary override here is a no-op in practice but kept for
// explicit symmetry with the other classes.
[mat-flat-button].mat-primary {
--mat-button-filled-container-color: var(--mat-sys-primary);
--mat-button-filled-label-text-color: var(--mat-sys-on-primary);
}
[mat-flat-button].mat-error,
[mat-flat-button].mat-warn {
--mat-button-filled-container-color: var(--mat-sys-error);
--mat-button-filled-label-text-color: var(--mat-sys-on-error);
}
[mat-flat-button].mat-accent {
--mat-button-filled-container-color: var(--mat-sys-tertiary);
--mat-button-filled-label-text-color: var(--mat-sys-on-tertiary);
}
// mat-icon-button — class on the <button> element
[mat-icon-button].mat-primary {
--mat-icon-button-icon-color: var(--mat-sys-primary);
}
[mat-icon-button].mat-error,
[mat-icon-button].mat-warn {
--mat-icon-button-icon-color: var(--mat-sys-error);
}
[mat-icon-button].mat-accent {
--mat-icon-button-icon-color: var(--mat-sys-tertiary);
}
// mat-icon — class on the <mat-icon> element
// The component reads --mat-icon-color; its own compiled rule
// "mat-icon.mat-primary { color: var(--mat-icon-color, inherit) }" picks up the value.
mat-icon.mat-primary {
--mat-icon-color: var(--mat-sys-primary);
}
mat-icon.mat-error,
mat-icon.mat-warn {
--mat-icon-color: var(--mat-sys-error);
}
mat-icon.mat-accent {
--mat-icon-color: var(--mat-sys-tertiary);
}
// ── Background + foreground helpers ──────────────────────────────────────────
// .bg-mat-primary is used on mat-toolbar and any element that should have a
// primary-colored background. Beyond the background itself, we must also
// override the component-level tokens that child mat-button and mat-icon-button
// use for their own text/icon colors — otherwise text buttons inside a primary
// toolbar show blue-on-blue (their default --mat-sys-primary label color).
.bg-mat-primary {
background-color: var(--mat-sys-primary) !important;
color: var(--mat-sys-on-primary) !important;
// Toolbar's own text token (used for non-button content such as the title)
--mat-toolbar-container-text-color: var(--mat-sys-on-primary);
// Text buttons inside the toolbar
--mat-button-text-label-text-color: var(--mat-sys-on-primary);
--mat-button-text-state-layer-color: var(--mat-sys-on-primary);
// Icon buttons inside the toolbar (unless a more-specific override like
// .mat-error is also present on the button — that still takes precedence)
--mat-icon-button-icon-color: var(--mat-sys-on-primary);
--mat-icon-button-state-layer-color: var(--mat-sys-on-primary);
// Standalone mat-icon elements inside the element
--mat-icon-color: var(--mat-sys-on-primary);
}
// Generic text-color helpers for non-Material HTML elements
.mat-primary {
color: var(--mat-sys-primary);
}
.mat-error,
.mat-warn {
color: var(--mat-sys-error);
}
.mat-accent {
color: var(--mat-sys-tertiary);
}
Enabling Theme Toggle
(1) in your app.ts import the toggler component:
import { ThemeToggleComponent } from '@myrmidon/ngx-mat-tools';
// TODO: add ThemeToggleComponent to imports array
(2) in your app.html, add the toggler at the end of toolbar components, before the closing tag of the toolbar element:
<ngx-theme-toggle/>
Replacing Colors
To ensure everything is visible when toggling to dark mode, you must replace literal colors with Angular Material CSS variables. Here are some typical sample operations I performed on the general components:
(1) replace colors used in tables (e.g. the #e2e2e2 color used for alternating rows). You can use these styles for tables:
table {
width: 100%;
border-collapse: collapse;
}
tbody tr:nth-child(odd) {
background-color: var(--mat-sys-surface-container);
}
th {
text-align: left;
font-weight: normal;
color: var(--mat-sys-on-surface-variant);
}
tbody tr:hover {
background-color: var(--mat-sys-surface-container-high);
}
td.fit-width {
width: 1px;
white-space: nowrap;
}
tbody tr.selected {
background-color: var(--mat-sys-secondary-container);
color: var(--mat-sys-on-secondary-container);
}
(2) replace these typical colors:
silver:var(--mat-sys-on-surface-variant)red:var(--mat-sys-error)green:var(--mat-sys-success)white:var(--mat-sys-surface)