dark mode

Since most operating systems have introduced it, Dark Mode has become more and more established and is already available in many native apps. Web applications, on the other hand, seem not to have reached this trend yet. Therefore, this article shows how Dark Mode can be detected automatically in a web app based on Angular Material and how the color scheme can be adapted accordingly.

Introduction

Angular offers very good support in the design of its UI components. With the help of Angular Material color themes can be created that determine which colors should be used for which component. Two slightly different variants will be described in the following, which allow the implementation of an automatic Dark Mode detection. While the first approach is a solution which is realized exclusively via Sass, the second one extends this concept with TypeScript to provide more programmatic possibilities.

Requirements

First of all, Angular Material should be added to your Angular project:

Pure Sass approach

The Pure Sass approach describes a setup that enables an Angular application to automatically adapt to Dark Mode purely with Sass. With Angular Material, themes are defined in the global styles.scss file in the root directory of a typical Angular CLI project.

Themes can either be created using the mixin mat-light-theme if a theme with bright colors is desired. If the colors should be rather dark, the mat-dark-theme mixin should be used instead. These mixins expect the primary color as the first argument, an accent color as the second, and a warning color can be optionally set as third argument. All other colors of the theme are then derived from these main colors.

Both light and dark theme should be defined like this:

// Imports for Angular Material Theming
@import '~@angular/material/theming';
@include mat-core();

// Light theme
$light-primary: mat-palette($mat-indigo);
$light-accent:  mat-palette($mat-pink, A200, A100, A400);
$light-theme:   mat-light-theme($light-primary, $light-accent);

// Dark theme
$dark-primary: mat-palette($mat-blue-grey);
$dark-accent:  mat-palette($mat-amber, A200, A100, A400);
$dark-warn:    mat-palette($mat-deep-orange);
$dark-theme:   mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

Depending on which color scheme is preferred by the user, the according theme has to be included. For this purpose most browsers support the CSS media feature prefers-color-scheme. It can detect whether a user prefers a light or dark theme in the system settings.

Angular Material provides the Sass mixin angular-material-theme which applies the selected theme to all default components of Angular Material:

@media (prefers-color-scheme: light) {
  @include angular-material-theme($light-theme);
}

@media (prefers-color-scheme: dark) {
  @include angular-material-theme($dark-theme);
}

Of course not only the default components of Angular Material should apply the theme, but also our own custom components. For example, there could be a component for our side navigation called sidenav. Inside of the file sidenav.component.scss a mixin called sidenav-theme is defined which is responsible for the styles of this component. With the function map-get one can get access to the according theme colors. The Sass function mat-color can retrieve either colors from the Material Color Palette or a relative color to a theme color (e.g. a darker hue):

@mixin sidenav-theme($theme) {
  $primary: map-get($theme, primary);
  $warn: map-get($theme, warn);

  .sidenav-header {
    background: mat-color($primary, darker);
    color: mat-color($primary, default-contrast);
  }
}

In the next step the sidenav.component.scss file is imported into the global styles.scss file. Another mixin called custom-components-theme wraps all custom component mixins and provides the theme information for them:

// Import mixin of sidenav component
@import "app/sidenav/sidenav.component";

// Custom themable components
@mixin custom-components-theme($theme) {
  @include sidenav-theme($theme);
  // Include other custom component mixins here...
}

The custom-components-theme mixin is now included as well as the angular-material-theme mixin inside the CSS media feature prefers-color-scheme in the styles.scss file:

@media (prefers-color-scheme: light) {
  // Apply theme for default Angular Material components
  @include angular-material-theme($light-theme);
  
  // Apply theme for custom components
  @include custom-components-theme($light-theme);
}

@media (prefers-color-scheme: dark) {
  @include angular-material-theme($dark-theme);
  @include custom-components-theme($dark-theme);
}

Example App

Below an exemplary Angular application can be found that has implemented the Pure Sass approach. If you enable the Dark Mode in the settings of your operating system, the app will appear in dark colors, otherwise in bright colors.

https://stackblitz.com/edit/angular-dark-mode-tzuwn4?embed=1&file=src/app/app.component.ts

Dynamic approach

Similar to the Pure Sass approach, the dynamic approach is based on the themes defined in the global styles.scss file. The only difference is that the themes are now integrated into the Angular application via TypeScript. It gives a better control over the management of the themes and allows the development of more than two themes. Beyond that, the user can manually change the themes within the application itself.

To achieve that, the themes are made accessible via CSS classes in styles.scss:

.light-theme {
  @include angular-material-theme($light-theme);
  @include custom-components-theme($light-theme);
}

.dark-theme {
  @include angular-material-theme($dark-theme);
  @include custom-components-theme($dark-theme);
}

Now, the entire logic for managing the themes is getting outsourced to a service called ThemingService which basically consists of two properties:

  • themes List of all available themes
  • theme Name of the theme that is currently used

The property themes provides a list of all available themes that can be used for the application. These themes must of course be defined as CSS classes in the styles.scss file as described above. The current theme property has the type BehaviorSubjectso that other components of the application can observe this value and react to changes. It must always be initialized with a value from the beginning – in this case the default theme for the application (e.g. the light-theme).

Initially, the service checks if the value of the media query for prefers-color-scheme matches the Dark Mode. If this is the case, then the dark-theme is used as current theme, otherwise (even if the system does not provide a Dark Mode) the light-theme is used as default theme. With the addListener method the value of the media feature can be observed to update the ThemingService automatically if the preference changes:

@Injectable()
export class ThemingService {
  themes = ["dark-theme", "light-theme"]; // <- list all themes in this array
  theme = new BehaviorSubject("light-theme"); // <- initial theme

  constructor(private ref: ApplicationRef) {
    // Initially check if dark mode is enabled on system
    const darkModeOn =
      window.matchMedia &&
      window.matchMedia("(prefers-color-scheme: dark)").matches;
    
    // If dark mode is enabled then directly switch to the dark-theme
    if(darkModeOn){
      this.theme.next("dark-theme");
    }

    // Watch for changes of the preference
    window.matchMedia("(prefers-color-scheme: dark)").addListener(e => {
      const turnOn = e.matches;
      this.theme.next(turnOn ? "dark-theme" : "light-theme");

      // Trigger refresh of UI
      this.ref.tick();
    });
  }
}

Finally, in the AppComponent (app.component.ts) the ThemingService is injected and in the ngOnInit method the theme property of the service is getting observed. The current theme is stored in the variable cssClass and via HostBinding the value of this variable is used as CSS class name of the AppComponent. So the corresponding theme from the styles.scss file is applied to this component. Due to the fact that AppComponent is the initial component of our Angular application, the theme is applied to all other child components:

export class AppComponent implements OnInit {
  constructor(private themingService: ThemingService) { }
  @HostBinding('class') public cssClass: string;

  ngOnInit() {
    this.themingService.theme.subscribe((theme: string) => {
      this.cssClass = theme;
    });
  }
}

Example App

Below an exemplary Angular application can be found that has implemented the dynamic approach. If you enable the Dark Mode in the settings of your operating system, the app will appear in dark colors, otherwise in bright colors. In addition, you can switch the theme manually by clicking on the paint bucket in the upper right corner.

Conclusion

This article showed how to detect the Dark Mode in an Angular application and how the color scheme of all components can be adapted accordingly by using themes. Basically, the CSS media feature prefers-color-scheme plays a central role, as it allows to detect if the user has requested the system to use a light or dark color theme. Because Angular Material already comes with dark and light themes by default with all components being dependent on them, it is relatively easy to display the entire application in different styles.

In conclusion, it can be said that with Angular it is very well possible to recognize a user’s preferred theme with both a Pure Sass method as well as with a TypeScript variant. This offers great possibilities to make web applications more flexible and more comfortable for the user regarding the visual appearance.

Blurred CSS Background

LEAVE A REPLY

Please enter your comment!
Please enter your name here