SASS-only Material Angular 2 Palette generator (2025 Update)

This is an updated version of my old article SASS-only Material Angular Palette generator published in 2020. It’s updated to be compatible with Angular 18+ and Material 2.

mat-palette-gen.scss
@use 'sass:math';
@use 'sass:map';
@use 'sass:color';

$values: (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, A100, A200, A400, A700);

@mixin _output-palette-variables($map, $prefix) {
  @each $key, $value in $map {
    @if ($value) {
      @if ($key == 'contrast') {
        @include _output-palette-variables($value, '#{$prefix}-contrast');
      } @else {
        --#{$prefix}-#{$key}: #{$value};
      }
    }
  }
}

@function createPalette($color) {

  $white: #fff;
  $black: #000;
  $baseDark: multiply($color, $color);

  $palette: (
    50 : color.mix($color, $white, 12%),
    100 : color.mix($color, $white, 30%),
    200 : color.mix($color, $white, 50%),
    300 : color.mix($color, $white, 70%),
    400 : color.mix($color, $white, 85%),
    500 : color.mix($color, $white, 100%),
    600 : color.mix($color, $baseDark, 87%),
    700 : color.mix($color, $baseDark, 70%),
    800 : color.mix($color, $baseDark, 54%),
    900 : color.mix($color, $baseDark, 25%),
    A100: color.adjust(color.mix($black, $baseDark, 15%), $lightness: 65%, $saturation: 80%),
    A200: color.adjust(color.mix($black, $baseDark, 15%), $lightness: 55%, $saturation: 80%),
    A400: color.adjust(color.mix($black, $baseDark, 15%), $lightness: 45%, $saturation: 100%),
    A700: color.adjust(color.mix($black, $baseDark, 15%), $lightness: 40%, $saturation: 100%),
  );

  $contrast: ();
  @each $v in $values {
    $contrast: map.merge($contrast, ($v: getContrast(map.get($palette, $v))))
  }

  $palette: map.merge($palette, (contrast: $contrast));

  @return $palette;
}


@function multiply($rgb1, $rgb2) {
  $r: math.floor(math.div(color.channel($rgb1, "red", $space: rgb) * color.channel($rgb2, "red", $space: rgb), 255));
  $g: math.floor(math.div(color.channel($rgb1, "green", $space: rgb) * color.channel($rgb2, "green", $space: rgb), 255));
  $b: math.floor(math.div(color.channel($rgb1, "blue", $space: rgb) * color.channel($rgb2, "blue", $space: rgb), 255));
  @return rgb($r, $g, $b);
}

@function getBrightness($color) {
  @return math.div(color.channel($color, "red", $space: rgb) * 299 + color.channel($color, "green", $space: rgb) * 587 + color.channel($color, "blue", $space: rgb) * 114, 1000);
}

@function isLight($color) {
  @return getBrightness($color) >= 128;
}

@function getContrast($color) {
  @if isLight($color) {
    @return #000;
  } @else {
    @return #fff;
  }
}

Usage

SCSS
@use '@angular/material' as mat;
@use 'sass:map';

// If you want the `mat-app-background` class
@include mat.app-background();

$primary-color: #6738d6;
$accent-color: #ebb652;
$warn-color: #d13212;

$primary-palette: createPalette($primary-color);
$accent-palette: createPalette($accent-color);
$warn-palette: createPalette($warn-color);

$primary: mat.m2-define-palette($primary-palette);
$accent: mat.m2-define-palette($accent-palette);
$warn: mat.m2-define-palette($warn-palette);

$theme: mat.m2-define-light-theme((
  color: (
    primary: $primary,
    accent: $accent,
    warn: $warn
  ),
));

// If you want the contrast colors
$primary-contrast-color: map.get(map.get($primary-palette, 'contrast'), 500);
$accent-contrast-color: map.get(map.get($accent-palette, 'contrast'), 500);
$warn-contrast-color: map.get(map.get($warn-palette, 'contrast'), 500);

// If you want to have CSS vars
:root {
  --primary: #{$primary-color};
  --accent: #{$accent-color};
  --warn: #{$error-color};
  --primary-contrast: #{$primary-contrast-color};
  --accent-contrast: #{$accent-contrast-color};
  --warn-contrast: #{$warn-contrast-color};
  
  /* 
  These will output ALL the palette colors as CSS vars
  
  EXAMPLE:
  @include _output-palette-variables($primary-palette, 'primary')
  
  OUTPUT:
     --primary-50: rgb(236.76, 231.12, 250.08);
     --primary-100: rgb(209.4, 195.3, 242.7);
     --primary-200: rgb(179, 155.5, 234.5);
     --primary-300: rgb(148.6, 115.7, 226.3);
     --primary-400: rgb(125.8, 85.85, 220.15);
     --primary-500: #6738d6;
     --primary-600: rgb(94.94, 50.28, 209.45);
     --primary-700: rgb(84.4, 42.8, 203.5);
     --primary-800: rgb(74.48, 35.76, 197.9);
     --primary-900: rgb(56.5, 23, 187.75);
     --primary-A100: hsl(250.4191616766, 167.4345549738%, 96.8333333333%);
     --primary-A200: hsl(250.4191616766, 167.4345549738%, 86.8333333333%);
     --primary-A400: hsl(250.4191616766, 187.4345549738%, 76.8333333333%);
     --primary-A700: hsl(250.4191616766, 187.4345549738%, 71.8333333333%);
     --primary-contrast-50: #000;
     --primary-contrast-100: #000;
     --primary-contrast-200: #000;
     --primary-contrast-300: #000;
     --primary-contrast-400: #fff;
     --primary-contrast-500: #fff;
     --primary-contrast-600: #fff;
     --primary-contrast-700: #fff;
     --primary-contrast-800: #fff;
     --primary-contrast-900: #fff;
     --primary-contrast-A100: #000;
     --primary-contrast-A200: #000;
     --primary-contrast-A400: #fff;
     --primary-contrast-A700: #fff;
   */ 
  @include _output-palette-variables($primary-palette, 'primary');
  @include _output-palette-variables($accent-palette, 'accent');
  @include _output-palette-variables($warn-palette, 'warn');
}

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

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.