angularangular-material

How do I alter the hover, text and background colours of some CDK angular material components like mat-menu-item and mat-select?


How do I get it to use my theme's primary and accent colours for example?

I have set up my theme using material 2 custom palette as such. I have excluded the palette setup for brevity

$theme: mat.m2-define-dark-theme(
  (
    color: (
      primary: $primary,
      accent: $accent,
      warn: $warn,
    ),
    typography: mat.m2-define-typography-config(),
    density: -2,
  )
);

@include mat.core();
@include mat.all-component-themes($theme);
@include shared.shared-themes($theme);
@include components.component-themes($theme);

Solution

  • Angular material mat-menu items use the foreground and background colours of the theme. This is why when you toggle the theme on the angular material website it does not use primary or accent colours, but toggles between a white and grey colour depending on if it's a light or dark theme selected:

    When looking at the select when choosing the purple theme on their site you can see each option is either a grey colour in light mode or a black colour in dark mode:

    enter image description here

    Turning our attention back to the menu-items this, can be corroborated by looking at the source in @node_modules/@angular/material/core/tokens/m2/mat_menu.scss (Angular 18 if using Material 2)

    We see in the file that the colours are generally dependent on the foreground and background colours:

    // Tokens that can be configured through Angular Material's color theming API.
    @function get-color-tokens($theme) {
      $is-dark: inspection.get-theme-type($theme) == .dark;
      $active-state-layer-color: inspection.get-theme-color($theme, foreground, base,
        if($is-dark, .08, .04));
      $text-color: inspection.get-theme-color($theme, foreground, text);
    
      @return (
        item-label-text-color: $text-color,
        item-icon-color: $text-color,
        item-hover-state-layer-color: $active-state-layer-color,
        item-focus-state-layer-color: $active-state-layer-color,
        container-color: inspection.get-theme-color($theme, background, card),
        divider-color: inspection.get-theme-color($theme, foreground, divider),
      );
    }
    

    These colours can be altered application-wide via the theme's contrast of your $primary palette. Not recommended due to the number of component's dependent on it. This is mentioned in one of the answers here: Angular 2 Material Foreground

    There is also a github issue on how to make a sweeping change to this: https://github.com/angular/components/issues/6244

    The above might help for you in some circumstances where you want a very sweeping change.

    But lets look at modifying a only, by way of example, a mat-menu-item.

    For now, let's also assume you want to make the change on a particular component, and not application wide, such as a navbar that uses the mat-menu-item

    Many solutions give up on being clean, if it can't be done using the palettes, and try to override the background-color directly. However, you now have a dependency on angular material's classes in your CSS. So if angular material changes their classnames in version updates, your code will break. Additionally, you would need to use the deprecated ng-deep.

    To solve this, you might add a class to the mat-menu-item component, and override the background-color or color on the class. This is better. But you could end up still needing to write very long CSS to make sure your override has higher specificity than angular material's CSS to take effect. Again, if Angular Material's internal code changes, it's CSS specificity might trump yours.

    The trick is to use your own class and override the CSS variables that are at the html level and so do not have specificity you need to compete with.

    So if you inspect the css of your mat-menu-item you'll see the a focussed state colour, for example is set like this:

    enter image description here

    If we trawl further down our CSS on the mat-menu-item we find the below section on the HTML level:

    enter image description here

    So lets say we want to alter the background color. We're probably interested in changing --mat-menu-item-focus-state-layer-color and --mat-menu-item-hover-state-layer-color

    And so the final solution to this problem is as follows:

    <button class="custom-menu-list-item" mat-menu-item>
    My item
    </button>
    

    And in our *component.theme.scss:

    .custom-menu-list-item{
       --mat-menu-item-focus-state-layer-color: red;
       --mat-menu-item-hover-state-layer-color: red;
    }
    

    Or preferably, from a palette an example would be:

     --mat-menu-item-hover-state-layer-color: #{mat.m2-get-color-from-palette($blue-primary, 500)}
    

    And that's all there is to it. A final solution could look something like the following:

      .custom-menu-list-item {
        --mat-menu-item-focus-state-layer-color: #{mat.m2-get-color-from-palette($purple-primary, 500)};
        --mat-menu-item-hover-state-layer-color: #{mat.m2-get-color-from-palette($purple-primary, 500)};
        --mat-menu-item-label-text-color: #{mat.m2-get-color-from-palette($blue-primary 500)};
      }
    
      .custom-menu-list-item:hover {
        --mat-menu-item-label-text-color: white;
      }
    

    It is worth mentioning in some scenarios Angular material doesn't have a CSS variable for a circumstance you have in mind. For example in a nested mat-menu the text color is defined via a single variable, that is used for both highlighted and unhighlighted menu items on the path you've traversed through the menu. If you want a different color for highlighted items you'd have to fall back on using the angular material component selector, or in the worst case the classes combined with your class to achieve higher specificity:

    mat-menu-item{
      --mat-menu-item-label-text-color: white;
    }
    
    .custom-menu-list-item.nav-menu-list-item.mat-mdc-menu-item-highlighted {
        --mat-menu-item-label-text-color: white;
    }
    

    If you want to make the change system-wide on all mat-menu then you can put it on the body element in one of your application's root theme files, to override the CSS variables on html element.

    If you've defined a particular theme class due to having multiple themes, which is quite common, overriding the variable there won't work right away.

    This is because your theme class will be defined on a div such as the themeClass$ observable below

    <div class="mat-app-background" [class]="themeClass$ | async">
      <app-nav-menu></app-nav-menu>
      <div class="scroll-container">
        <router-outlet></router-outlet>
      </div>
    </div>
    
    .my-light-theme{
      --mat-menu-item-hover-state-layer-color: red;
    }
    

    This is because components that use the CDK overlay like the mat-option in a select, modals and menu-items render outside of the angular application.

    So you will have to use one of the solutions that put your theme class on the CDKOverlay as well. There are other stackoverflow solutions out there that explain how to do this. It involves injecting private overlayContainer: OverlayContainer into a high level component and adding classes to the overlayContainers classList