Back Copy 3 back
image

An approach to dynamic theme color generation in Angular

Stefan Peshikj November 25, 2020
share this on

Designing and theming is one of the most important parts of the Web Development process. In theory, a web application is always created to solve a specific set of problems for the end-users, and as long as the application fulfills its primary goal, it will always attract the users which require that solution.

A good thing to remember is that a beautiful and appealing website with a great user experience will always attract even more customers since the application will be easier to use. Along with all the UX improvements, this is also where the brand’s specific visual identity comes into play.

How do we as developers tackle these problems?

Most of the time our job while developing is to follow one certain visual identity pattern and create a lovely application from that.

But what happens when we want our application to have one visual identity for some users, and a different one for others?

What happens if our application needs to support a large range of different visual identities?

In this blog, we will try to provide some insight using a sample solution that showcases an example project which utilizes Angular in an attempt to generate a wide range of different theme variations using only a couple of colors.

NOTE: This approach is not directly tied to Angular, i.e. we could use plain JavaScript code and get the same result, which is always a bonus as we are not tied to any specific framework, though some framework capabilities will make some steps easier to implement and use (eg: APP_INITIALIZER in Angular which helped us in one of the steps to the solution).

What is the end result of our work?

The end result is a simple website where we could play with a lot of theme options and find the best look for our application. It consists only of one page where we showcase the capabilities that are easily applicable to multiple pages and a lot of components, all of which could be themeable.

The full code for this example can be found on Github.

Two versions of the same website using different themes

What are our requirements?

In order to start solving an issue, we need to be presented with certain problems. In this example, we try to tackle the following problems:

  • We need to create different themes for our application, and they can be comprised of any random colors
  • We should be able to change colors for certain components on demand
    Even if two versions of our website will use the same theme colors, some components on the first website could be themed differently from the ones on the second website.
  • We should be able to theme anything we want
  • We should only define a couple of colors, and create a theme only from that
  • We should be able to theme the Angular Material design components using the colors that we’ve specified
  • We should get the theming information from an external source (a webserver fetching them from a database)
  • If no theme is found, we should fall back to some default theme

If some of the requirements mentioned above are something that you can relate to, then you could find some ideas about a potential solution to the problem.

What challenges are we facing?

Challenge #1: Fetch theming data from server

If we’ve analyzed the issues above, we surely noticed that we need to fetch the theming information from a database, so this means that we need to do an asynchronous request to a web server. But what does this mean for our application?

Well, one take at it is to probably load the application and draw it with some default theming data while making the HTTP request, and only after we get the result from the server, we should switch over to using a different theme.

While it’s an option, this clearly should not be part of our scope. The end-user does not need to know that there are multiple variations of the website, he just wants to see his own version. It’s quite a pretty bad user experience to load the page, show that version for 200ms, and then all of a sudden change all the colors on the page. This flickering between two different states can surely decrease your audience.

This is a good place where we could put Angular’s APP_INITIALIZER injection token to good use.

The provided functions are injected at application startup and executed during app initialization. If any of these functions returns a Promise, initialization does not complete until the Promise is resolved.

We could potentially try to return a Promise where we could load the theme from the service, set up all the theme variables, and then return resolve the Promise to allow Angular to continue with the initialization. Let’s try it out by providing the injection token in our AppModule .

providers: [
  {
    provide: APP_INITIALIZER,
    // Provide the APP_INITIALIZER, wait until the theming configuration is fetched and set up correctly
    useFactory: (themingService: ThemingService) =>
      () => themingService.initialize(),
    deps: [ThemingService],
    multi: true
  }
]

Our theming service is initialized by either sending an HTTP request to some web server so that it can return the theming information so that we can generate all the color palettes and component variables. For the sake of simplicity, the demo project uses an in-memory approach as we have a default theme that is ready for use.

initialize(): Promise<any> {
  this.subscribeToThemeChanges();

  // httpClient.get("some_url/some_theme_id").toPromise()
  // use some browser storage mechanisms and caching as well
  // will use a local object for simplicity
  return observableOf(DEFAULT_THEME as ThemeConfig)
    .pipe(
      // If some error happens, use some default theme
      catchError(() => observableOf(DEFAULT_THEME)),
      // This could even be a syncronous process,
      // but we will use a subject for this example
      tap((themeConfig) => this.currentTheme.next(themeConfig))
    )
    .toPromise();
}private subscribeToThemeChanges(): void {
  this.currentTheme.subscribe((themeConfig: ThemeConfig) => {
    this.setupMainPalettes(themeConfig);
    this.setupComponentVariables(themeConfig);
  });
}

Challenge #2: Use the theming variables in CSS

Great, now we know that we can fetch the theme from the server which will give us enough time to generate all of the theming variables for our application. The question is, how do we actually use these variables?

Simply put, we can’t use pure SCSS variables, as they will be pre-processed into CSS values and we could not use our theming variations.

This means that we must use plain CSS variables because they can be updated whenever and wherever we want. But where do we add these variables?

For the sake of simplicity, we inserted these variables in the root <html> tag in the application, but they can be inserted and altered per-component.

We must also change the Angular Material design theme if we want to use the material design components. That’s all great but Angular Material requires a color palette which is comprised of 14 variations of one color, along with additional 14 contrast colors.

$theme-primary-color-map: (
  50: var(--theme-primary-color-50),
  100: var(--theme-primary-color-100),
  200: var(--theme-primary-color-200),
  300: var(--theme-primary-color-300),
  400: var(--theme-primary-color-400),
  500: var(--theme-primary-color-500),
  600: var(--theme-primary-color-600),
  700: var(--theme-primary-color-700),
  800: var(--theme-primary-color-800),
  900: var(--theme-primary-color-900),
  A100: var(--theme-primary-color-A100),
  A200: var(--theme-primary-color-A200),
  A400: var(--theme-primary-color-A400),
  A700: var(--theme-primary-color-A700),
  contrast: (
    50: var(--theme-primary-color-contrast-50),
    100: var(--theme-primary-color-contrast-100),
    200: var(--theme-primary-color-contrast-200),
    300: var(--theme-primary-color-contrast-300),
    400: var(--theme-primary-color-contrast-400),
    500: var(--theme-primary-color-contrast-500),
    600: var(--theme-primary-color-contrast-600),
    700: var(--theme-primary-color-contrast-700),
    800: var(--theme-primary-color-contrast-800),
    900: var(--theme-primary-color-contrast-900),
    A100: var(--theme-primary-color-contrast-A100),
    A200: var(--theme-primary-color-contrast-A200),
    A400: var(--theme-primary-color-contrast-A400),
    A700: var(--theme-primary-color-contrast-A700),
  ),
);
$theme-primary-palette: mat-palette($theme-primary-color-map);

By using CSS variables in our color map, we can represent a Material palette without the need to specify the colors before-hand, and it will not cause issues in the SCSS compilation process. Maybe this will not be supported in the future, as the variables that we are declaring do not have any values yet, but the application will compile so we are good to go. Superb, we need to only generate these variations now!

Since the colors for the official Material palettes are sometimes altered by a designer, we can never generate an exact copy of some material theme, but the closest thing we found is the Constantin color algorithm logic used in a palette generator created by this library.

We manipulate and mix the colors using the tinycolor JS library.

import * as tinycolor from 'tinycolor2';

export interface ColorConfig {
  colorVariant: string;
  colorHexValue: string;
  shouldHaveDarkContrast: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class ThemingService implements OnDestroy {

  private generateColorPalette(hexColor: string): Array<ColorConfig> {
    const baseLight = tinycolor('#ffffff');
    const baseDark = this.multiply(tinycolor(hexColor).toRgb(), tinycolor(hexColor).toRgb());
    const baseTriad = tinycolor(hexColor).tetrad();

    return [
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 12), '50'),
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 30), '100'),
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 50), '200'),
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 70), '300'),
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 85), '400'),
      this.mapColorConfig(tinycolor.mix(baseLight, hexColor, 100), '500'),
      this.mapColorConfig(tinycolor.mix(baseDark, hexColor, 87), '600'),
      this.mapColorConfig(tinycolor.mix(baseDark, hexColor, 70), '700'),
      this.mapColorConfig(tinycolor.mix(baseDark, hexColor, 54), '800'),
      this.mapColorConfig(tinycolor.mix(baseDark, hexColor, 25), '900'),
      this.mapColorConfig(tinycolor.mix(baseDark, baseTriad[3], 15).saturate(80).lighten(65), 'A100'),
      this.mapColorConfig(tinycolor.mix(baseDark, baseTriad[3], 15).saturate(80).lighten(55), 'A200'),
      this.mapColorConfig(tinycolor.mix(baseDark, baseTriad[3], 15).saturate(100).lighten(45), 'A400'),
      this.mapColorConfig(tinycolor.mix(baseDark, baseTriad[3], 15).saturate(100).lighten(40), 'A700')
    ];
  }

  /**
   * Map the color and its variant to something that we understand
   * Also check if we need to use a light or dark contrast color
   * @param tinyColorInstance
   * @param colorVariant
   * @private
   */
  private mapColorConfig(tinyColorInstance: tinycolor.Instance, colorVariant: string): ColorConfig {
    return {
      colorVariant,
      colorHexValue: tinyColorInstance.toHexString(),
      shouldHaveDarkContrast: tinyColorInstance.isLight()
    };
  }
}

Challenge #3: Use the CSS variables in our components

Once we have generated our variables, it’s up to figure out how to actually use them in our components. Note that our choice to use SCSS variables for accessing the CSS variables is optional and not really needed.

$theme-hero-content-background-color: var(--theme-hero-content-background-color);

We could also use the Angular Material SCSS mixins to help us when selecting the color variations we want. For example, we could access a certain variation of our primary color using the mat-color mixin:

background-color: mat-color($theme-primary-palette, 900);
color: mat-color($theme-primary-palette, '900-contrast');

We are done! Once we’ve established our variables and our work pattern, we can theme all of the components we want.

Since all the data is being returned from the server, we can decide to theme anything we want (e.g. fonts). Additionally, we do not need to import specific theme files for a certain theme because this is all done via JS code.

The demo project example also transforms all the colors we use into HSL format variables, so we can easily manipulate all of our colors and change their properties (ex: change the lightness value).

$theme-hero-content-lighter-background-color: hsl(
    var(--theme-hero-content-background-color-h),
    var(--theme-hero-content-background-color-s),
    calc(var(--theme-hero-content-background-color-l) / 0.7)
);

An example of a themed component

Conclusion

In this article, we’ve described one approach, although there are many, each with their own pros and cons. Please feel free to share your experiences and post questions in the comments.

Last but not least, you can find the simple application created for this example application on Github.

Happy coding! 🙂