Implementing Material 3 Theming in Angular 20
A practical guide to Material 3 (M3) theming in Angular 20: how M3's semantic, role-based system works, setup with SCSS, design tokens, Nx monorepo and Tailwind integration, and the common gotchas when migrating from Material 2.
When you’re building a new app, design can make or break how users experience it. Strong functionality with weak visual design often feels unfinished or hard to use. Many teams start with basic CSS or SCSS, but over time that can turn into a tangle of competing styles, !important overrides, and inconsistent sizes, colors, and fonts. A better option would be to implement Angular Material Design (version 3, referred to as M3). Material 3’s theming system aims to turn that chaos into a consistent, adaptable design language.
Your Next Angular Project Needs M3, Here’s Why…
First, it has something called adaptive color. This means colors can change based on the user’s context. For example, if your device is set to dark mode, M3’s adaptive color will automatically adjust to match your system settings.
Essentially, adaptive color is Material’s ability to respond to system-level color schemes like dark or light mode. Additionally, “dynamic palettes” (see below) can be generated programmatically or via user/system input to further customize the theme.
Another great thing about M3 is the dynamic palettes. This essentially allows you to take a few colors and M3 will generate a complete palette for you. This can be done “on the fly” or beforehand and set up as a theme.
M3 isn’t just about aesthetics though—it opens the door for much stronger accessibility by default. According to the M3 documentation found here:
“Accessibility standards are built into Material components, providing a foundation for inclusive product design. Anticipating a wide range of human experiences and disabilities prevents costly redesigns, reduces tech and design debt, and conserves resources upfront.”
Simply put, accessibility is in the DNA of what makes M3 so great. It’s literally written into the code, not only to keep from “reinventing the wheel” but also to make it easier for developers to create applications just about ANYONE can use.
Now that you understand the power behind M3 a little better, let’s talk about who will get the most from this article.
So who is this article for?
If your team is currently thinking about switching from Material 2 to M3, you’ll learn the basics of implementation and some of the things to watch out for when making the switch. Maybe you’re managing multiple Nx monorepos and want to handle theming across several applications—in that case, you’ll see how to share themes across libraries and keep things organized. Perhaps you’re an Angular developer looking for a scalable design system. Either way, you’ve come to the right place.
Scope and expectations
In this article we’ll walk through the fundamentals of M3, examine the setup basics, take a look at some real world “gotchas”, explore Nx implementation, see how Tailwind integration works and check out some best practices. That’s enough groundwork-laying. Let’s take a look at how Material theming has evolved.
Material 3 Theming Overview
Material 2 (M2)
M2 was a robust system that allowed greater customization, maintained many of the original Material design aspects, and became more “palette-centric.” It used mechanisms such as mat-light-theme and mat-palette to group specific palettes together for semi-customization. These were built on the basis of primary, accent, and warn palettes, which were applied in a very specific way.
Material 3 design philosophy
With the advent of M3 came a shift in the over-all structure and philosophy behind the Material Design system. Instead of being palette driven, it was more semantic-role based. So instead of just applying primary/accent/warn somewhat arbitrarily (at least at first glance), it used very specific identifiers. So in addition to your standard primary color, you would also have things like primary-container and surface which would give names to specific areas to make them easier to both identify and keep track of very specific elements of the UI. Almost as if you went from paint-by-number to a full paint palette and an empty canvas.
Core M3 theming elements
Dynamic color & tonal palettes:
Now instead of being stuck with mostly just preset palettes, we can generate new palettes based off of any given color. We can also give users the option of letting the app decide the color, based on either personal preference or system variables (such as if your machine is using dark mode).
Surface blending and elevation:
M2 felt very “flat” and it had a distinct style to it which was unique at the time. When M3 was introduced, that all changed. Now a lot more attention was paid to make the UI feel like there’s a bit of elevation to some elements, almost as if some are free floating. Using both shadows and tonal variations to give the illusion of depth.
Adaptive color roles (background, surface, outline, etc.):
Much of the M3 paradigm is role-based, so another aspect of M3 called ‘Adaptive color roles’ fits right in. It gives specific roles (outlines, surfaces… and so on) colors meant to lend to that role. So things like outlines have neutral stroke colors to be visible but not dominant. Let’s see how these differences look in code.
M2 vs M3 theming in code
M2 example
Here we have the basics for implementing theming in M2. Importing the necessary dependencies, including the mat-core which houses the prerequisite styles and the building blocks of mat-theming. Next we set the primary, accent, and warn palettes, implement them as a theme, and finally apply the theme to the app. Note: be sure to include this file in the global styles array in your angular.json file.
// 1. Import Angular Material M2 theming API
@import '~@angular/material/theming';
// 2. Core styles (once per app)
@include mat-core();
// 3. Define your three main palettes
$my-primary: mat-palette($mat-indigo);
$my-accent: mat-palette($mat-pink, A200, A100, A400);
$my-warn: mat-palette($mat-red);
// 4. Create a light theme
$my-app-theme: mat-light-theme($my-primary, $my-accent, $my-warn);
// 5. Apply the theme to all Material components
@include angular-material-theme($my-app-theme);
Fairly straight forward and easy to understand, although (in hind-sight) quite limited. Now let’s take a look at M3:
M3 example
In a similar fashion, when we import the necessary modules, we include the mat-core piece, but everything after that has changed. Now we go directly to applying something within the HTML CSS (or SCSS in this case) selector. Much of it is self explanatory (or explained in code comments) but readers should note the initial application of the primary palette. With this a user is not locked into any given palette, but it is good to have a baseline or default. This will fill in gaps where we may not want to be that granular, or maybe we just don’t know that something needs a style/color. This is just hinting at it, but notice the background and color style definitions at the bottom - this is utilizing tokens to adjust specific systemic elements. More on that later.
@use '@angular/material' as mat;
// Core Material styles (once per app)
@include mat.core();
// Global M3 theme
html {
// Let the browser know we support both light & dark; optional but recommended
color-scheme: light dark;
@include mat.theme((
color: (
primary: mat.$blue-palette,
theme-type: light,
),
typography: Roboto,
density: 0,
));
}
// Basic body styles using Material tokens (optional but common)
body {
background: var(--mat-sys-color-surface);
color: var(--mat-sys-color-on-surface);
}
The biggest variation is the theme definition itself. We use completely different mechanisms to implement a theme. Much of the previous tools such as mat-light-theme and mat-palette are now deprecated in M3. With the concepts and structure in place, let’s walk through wiring up a Material 3 theme in a real Angular 20 project.
Getting Started with M3 Theming in Angular 20
Now that you have a rough understanding of M3 and how it works, let’s look at how to implement it in your existing Angular app. This guide assumes you’re using Angular version 20.2 or higher and working with SCSS files.
First, install the versions of @angular/material and @angular/cdk that match your Angular version. For example, if you’re using Angular 20.2, install version 20.2 of both packages:
npm install @angular/material@20.2 @angular/cdk@20.2
Next, you’ll want to add and/or update your configuration files. If you’re using Nx, you’ll want to open up project.json and add something like the following:
{
"targets": {
"build": {
"executor": "@nx/vite:build", // or similar
"options": {
"styles": [
"apps/web/src/styles.scss" // <-- ensure this is .scss
],
"assets": [
"apps/web/src/favicon.ico",
"apps/web/src/assets"
]
}
}
}
}
Adjust your paths accordingly. This will ensure your project is configured to use a global SCSS file.
Once that is configured, you’ll want to add in your material-theme file. Be sure to do something similar to the M3 example file above. In your main styles.scss file add the following lines:
@use "tailwindcss" as *; // assuming you're using tailwind
/* Angular Material M3 theme */
@use './theme/material-theme';
This is assuming you’ll be using Tailwind for css throughout your app. If this is the case, be sure to update your tailwind.config.js (or .ts if you’re using TypeScript) file like the following:
module.exports = {
content: [
'apps/web/src/**/*.{html,ts}', // plus any shared libs if needed
],
corePlugins: {
preflight: false, // critical to avoid Tailwind resetting Material components
},
theme: {
extend: {},
},
plugins: [],
};
Notice the preflight: false line. This will ensure that tailwind won’t reset your M3 component styles.
Note: As of Angular version 20.2, @angular/animations, BrowserAnimationsModule, and provideAnimations are all marked as deprecated with intent to remove them by version 23. While it is still possible to use them for now, Angular’s official guidance is to migrate to native CSS animations and animate.enter / animate.leave.
Once your first M3 theme is wired up, you’ll quickly run into real-world edge cases—especially with lazy-loaded modules and overrides. Let’s look at the most common gotchas and how to fix them.
Real-World Gotchas in M3 Theming
With a paradigm shift in going from M2 to M3, there were quite a few differences that although not completely unexpected, can sneak up on you. Here are a few examples of some of the issues that you might encounter and possibilities of how to fix them:
- One of the biggest issues when switching from M2 to M3 was that all of the theming functions, such as
mat-light-themeand so on, were all removed in lieu of the simplifiedmat.theme()approach. The move frommat.define-light-theme()ormat.define-dark-theme()now requires you to usemat.theme()to define the base theme. - Tailwind overriding Material CSS. The fix for this is included in the
tailwind.config.jsfile previously discussed. By settingcorePlugins: { preflight: false }, you disable Tailwind’s CSS reset which prevents it from interfering with the base styles of Material components. - The
mat.define-palette()functionality is gone. To address this, determine your color palette (or ask your design team) then map the colors accordingly using the M3 design tokens.
M3 Design Tokens
These are the new CSS variables (tokens) used by Material 3 for its color system:
--mat-sys-primary
--mat-sys-on-primary
--mat-sys-primary-container
--mat-sys-on-primary-container
--mat-sys-secondary
--mat-sys-on-secondary
--mat-sys-secondary-container
--mat-sys-on-secondary-container
--mat-sys-tertiary
--mat-sys-on-tertiary
--mat-sys-tertiary-container
--mat-sys-on-tertiary-container
--mat-sys-error
--mat-sys-on-error
--mat-sys-error-container
--mat-sys-on-error-container
--mat-sys-background
--mat-sys-on-background
--mat-sys-surface
--mat-sys-on-surface
--mat-sys-surface-variant
--mat-sys-on-surface-variant
--mat-sys-surface-tint
--mat-sys-inverse-surface
--mat-sys-inverse-on-surface
--mat-sys-inverse-primary
--mat-sys-outline
--mat-sys-outline-variant
--mat-sys-shadow
--mat-sys-scrim
--mat-sys-surface-dim
--mat-sys-surface-bright
--mat-sys-surface-container-lowest
--mat-sys-surface-container-low
--mat-sys-surface-container
--mat-sys-surface-container-high
--mat-sys-surface-container-highest
These are put to use by creating a theme file like this:
$light: (
color-fg-primary: #000000, // primary text
color-bg-primary: #ffffff, // primary background
...
)
$dark: (
color-fg-primary: #ffffff, // primary text
color-bg-primary: #000000, // primary background
...
)
This defines the raw color values for a light theme (and similarly for a dark theme). These maps are then wired into Material’s theming system via a mapper, like below:
@use '@angular/material' as mat;
@use './theme-file' as theme;
html {
@include mat.theme-overrides(
(
primary: map-get(theme.$light, color-bg-primary),
on-primary: map-get(theme.$light, color-fg-primary),
...
)
)
}
In the mapper, when we use mat.theme-overrides() and apply the default ($light) theme, we are overriding Material’s default design tokens using values from your $light map. The overrides such as primary and on-primary directly affect the css variables listed above. These specifically would be --mat-sys-primary for primary and --mat-sys-on-primary for on-primary.
Nx Monorepo Considerations
When working with M3 theming in an Nx monorepo, first decide whether your theme will be shared across multiple projects or libraries. Even if you don’t plan to share themes right now, keeping your theming files in an Nx library is still a good practice. This approach leads to a cleaner, more straightforward setup and makes it easy to share styles in the future if needed.
Shared Styling Across Multiple Applications
If you’ve decided to share your theming across multiple applications, first make sure your apps are in the same Nx monorepo. Then, set up a central shared styles Nx library—name it something like “shared-styles” or whatever you prefer. Inside this library, create a shared base styles file, a shared color palette file, a shared typography file, and an index.scss file.
In your index.scss file, forward the shared files:
@forward './shared-base'
@forward './shared-color-variables'
@forward './shared-typography'
This will allow you to add the shared files to your tsconfig.base.json paths like this:
{
"compilerOptions": {
"paths": {
"@app-shared-styles": ["libs/my-app/shared-styles/src/index.scss"]
}
}
}
Most other paths in this file will point to index.ts files, but referencing an SCSS file here is a well-established (if unofficial) practice that works reliably for sharing styles across projects. Just be aware that some IDEs or build tools may not fully support this pattern out of the box.
Integrating Material 3 with Tailwind CSS
Integrating Tailwind CSS with Material 3 is straightforward, but you’ll need to decide which framework’s styles should take precedence. Since this article focuses on Material UI, we’ll ensure Material styles are not overridden by Tailwind.
First, follow the standard installation process for Tailwind in your Angular and/or Nx environment. Once installed, update your tailwind.config.js (or tailwind.config.ts if you’re using TypeScript) as follows to disable Tailwind’s base reset:
module.exports = {
corePlugins: {
preflight: false, // Prevents Tailwind from resetting Material component styles
},
// ...other config
};
This setting ensures that Tailwind won’t override existing Material UI styles. If this seems familiar, it was briefly mentioned earlier in Getting Started with M3 Theming in Angular 20.
Next, add the following to your app’s styles.scss file to enable Tailwind’s utility classes:
@tailwind base;
@tailwind components;
@tailwind utilities;
That’s it! Tailwind should now work alongside Material UI without style conflicts.
Conclusion
To recap, Material 3 theming in Angular 20 introduces a modern, adaptable, and accessible approach to UI design. By moving from palette-centric to semantic-role-based theming, M3 enables dynamic color adaptation, improved accessibility, and easier maintenance.
In this article, we explored the differences between M2 and M3, walked through practical implementation steps—including Nx monorepo and Tailwind integration—and addressed common migration gotchas. With these tools and best practices, you’re equipped to build scalable, visually consistent, and inclusive Angular applications.
Thanks for reading! I hope you found this dive into Material UI theming helpful. Learn from my mistakes and successes, and pass your knowledge along to the next generation of developers.
Further Reading
There were some things I didn’t go in depth into and also some things that were left out. If you’d like to read further, here are some links. One thing that would be a good follow up is the various ways of switching theme context (light to dark, and so on). As there are many ways to skin a… website, there are also many ways to achieve theme switching. Do some research and find the best method that fits your project!
Here are some links for further reading on the topic: