Switch between Light and dark theme for whole site

1 Answer 5126 Views
Styling / Themes
Venkat
Top achievements
Rank 1
Venkat asked on 19 May 2021, 01:32 AM

Hi,

I am using Kendo UI Material theme. I would like to switch between light and dark themes without reloading the whole site. May I know what are variables available that I can set in the code?

Dimitar
Telerik team
commented on 20 May 2021, 06:25 AM

Currently there is not a straightforward way of dynamically modifying the Sass-based themes at run time. For changing the styles dynamically, you can utilize an approach that will preload/load the entire stylesheets in the application based on an action (e.g button click or dropdown selection). You can refer to the following answer in StackOverflow that provides a solution for Vue (the second example in the answer):

You should be able to adapt this solution, by utilizing the Kendo Sass-based themes. You can utilize the unpkg CDN for loading the stylesheets:

1 Answer, 1 is accepted

Sort by
3
garri
Top achievements
Rank 2
Iron
Iron
Iron
answered on 09 Aug 2021, 10:06 AM | edited on 10 Aug 2021, 01:58 PM

I Implemented service for Angular following Dimitar's advice.

I published a sample application on GitHub: https://github.com/llgarrido/Kendo-UI-Angular-Dark-Ligth-Mode-Switcher-Example

 

When the web application starts for the first time, the service implements the default theme of the operating system (dark or light).

 

There are several ways to switch the theme:

  • When requested by the user within the application.
  • When requested by the user from the theme configuration in the operating system (through prefers-color-scheme feature CSS media query).

If user chooses the theme mode from the web application its configuration will persist in the local storage.

 

I do not recommend preloading the style-sheets of both themes at application startup, in fact, in the example application the required css is loaded at runtime when the service decides which theme to apply.

 

Instead of configuring angular.json to load both style-sheets at application startup…

 

…
"architect": {
    ...
    "build": {
        …
        options: {
          …
          "styles": [
            "src/styles\styles.scss",
            "src/styles/kendoui-theme-dark.scss",
            "src/styles/kendoui-theme-light.scsss"
          ],
        }
    },
    …
},
…

 

..we tell angular.json to not include these style-sheets as we are going to only lazy load one of them at a time at runtime:

…
"architect": {
    …
    "build": {
        …
        "options": {
          …
          "styles": 
			[
				"src/styles/styles.scss",
				{
					"inject": false,
					"input": "src/styles/kendoui-theme-dark.scss",
					"bundleName": "kendoui-dark"
				},
				{
					"inject": false,
					"input": "src/styles/kendoui-theme-light.scss",
					"bundleName": "kendoui-light"
				}
            ]
,
        }
    },
    …
},
…

 

variables.scss (Kendo UI theme SCSS variables file for both dark and light modes):

$border-radius: 2px;
$primary-palette-name: indigo;
$secondary-palette-name: pink;

 

This is the file you get after creating a theme with Kendo UI Angular Theme Builder with no "$theme-type" variable to define the dark or light mode, this variable will be set later on both dark and light scss endpoints.

kendoui-dependencies.scss (SCSS dependencies file for both dark and light Kendo UI components style sheets):

@import "./variables.scss";
@import "~@progress/kendo-theme-material/scss/grid/_index.scss";
@import "~@progress/kendo-theme-material/scss/button/_index.scss";

The sample application only needs the styles for the Grid and Button components.

Separated Kendo UI SCSS files for dark and light modes:

kendoui-theme-dark.scss:

$theme-type: dark;
@import './kendoui-dependencies.scss'

kendoui-theme-light.scss:

$theme-type: light;
@import './kendoui-dependencies.scss'


theme-settings.ts (User interface theme settings model):

/**
 * Settings for the visual appearance of the user interface.
 *
 * * @export
 * @class ThemeSettings
 */
export class ThemeSettings {
/**
 *
 * Dark mode status enabled.
 * @type {boolean}
 * @memberof ThemeSettings
 */
public darkMode: boolean = false;
}

 

theme.service.ts:

import { DOCUMENT } from '@angular/common';
import { Inject, Injectable, OnDestroy, Renderer2, RendererFactory2 } from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ThemeSettings } from './theme-settings';

/**
 *  User interface theme manager (dark or light).
 *
 *  It implements the default theme of the operating system.
 *
 *  This service can change the theme mode:
 *  - When requested by the user within the application.
 *  - When requested by the user from the theme configuration in the operating system.
 *
 *  If user chooses the theme mode from the web application its configuration is persisted in local storage.
 *
 * @export
 * @class ThemeService
 * @implements {OnDestroy}
 */
@Injectable({
  providedIn: 'root'
})
export class ThemeService implements OnDestroy {

  /** The light mode background css class name to be attached to HTML Body element. */
  private static readonly LIGHT_MODE_CSS_CLASS_NAME : string = 'theme-alternate';

  /** Name of the local storage key value to query and persist the theme settings. */
  private static readonly SETTINGS_KEY: string = 'theme';

  /**
   * CSS media feature that is used to detect if the user has requested a light or dark color theme.
   * The user might indicate this preference through an operating system setting (e.g. light or dark mode).
   * */
  private static readonly SYSTEM_DARK_MODE_MEDIA_QUERY = '(prefers-color-scheme: dark)';

  /** DOM renderer */
  private renderer: Renderer2;

  /** Settings for the visual appearance of the user interface. */
  private settings: ThemeSettings;

  private destructionSubject: Subject<void> | null = null;

  private _kendouiStylesheetHtmlElement?: HTMLLinkElement;

  /**
   * Load or create the Html element that should link the url of the stylesheet of the Kendo UI component theme.
   *
   * @readonly
   */
  get kendouiStylesheetHtmlElement() {
    if (!this._kendouiStylesheetHtmlElement)
    {
      this._kendouiStylesheetHtmlElement = this.renderer.createElement('link');
      this.renderer.setAttribute(this._kendouiStylesheetHtmlElement, 'rel', 'stylesheet');
      const headHtmlElement: HTMLHeadElement = this.document.getElementsByTagName('head')[0];
      this.renderer.appendChild(headHtmlElement, this._kendouiStylesheetHtmlElement)
    }
    return this._kendouiStylesheetHtmlElement;
  }

  /**
   * Serializes and sets the value of the pair identified by key to value in Local Storage, creating a new key/value pair if none existed for key previously.
   *
   * @param {string} key The name of the key you want to create/update.
   * @param {*} value The value you want to give the key you are creating/updating.
   * @private
   */
  private setLocalStorageItem(key: string, value: any): void
  {
    const serializedValue: string = JSON.stringify(value);
    localStorage.setItem(key, serializedValue);
  }

  /**
  * Returns and deserializes the current value associated with the given key in Local Storage, or null if the given key
  *
  * @param {string} key The name of the key you want to retrieve the value of.
  * @returns {*} The value of the key. If the key does not exist, undefined is returned.
  * @private
  */
  private getLocalStorageItem(key: string): any
  {
    const serializedValue: string | null = localStorage.getItem(key);
    const result: any = (serializedValue) ? JSON.parse(serializedValue) : undefined;
    return result;
  }

  /**
   * Removes the key/value pair with the given key from the list associated with the object in Local Storage, if a key/value pair with the given key exists.
   *
   * @param {string} key The name of the key you want to remove.
   * @private
   */
  private removeLocalStorageItem(key: string)
  {
    localStorage.removeItem(key);
  }

  /**
   * Constructor.
   * @param {Document} document Web page loaded in the browser and serves as an entry point into the web page's content, which is the DOM tree..
   * @param {RendererFactory2} rendererFactory Creates and initializes a custom renderer that implements the Renderer2 base class.
   */
  constructor(@Inject(DOCUMENT) private document: Document,
      rendererFactory: RendererFactory2)
  {
    this.renderer = rendererFactory.createRenderer(null, null);
    this.settings = this.getLocalStorageItem(ThemeService.SETTINGS_KEY);
    if (!this.settings)
    {
      this.settings = new ThemeSettings();
      this.settings.darkMode = this.isSystemDark();
      this.startSystemModeSynchronization();
    }
  }

  //** Check if the user Operative System theme preference is dark mode. */
  isSystemDark(): boolean
  {
    const result: boolean = this.document.defaultView?.matchMedia(ThemeService.SYSTEM_DARK_MODE_MEDIA_QUERY).matches!;
    return result;
  }

  /**
   * Set stylesheet url for Kendo UI components.
   * @param stylesheetFilePath Kendo UI components css style sheet file path.
   */
  private setKendoUiControlsMode(stylesheetFilePath: string): void
  {
    this.renderer.setProperty(this.kendouiStylesheetHtmlElement, 'href', stylesheetFilePath);
  }

  /**
   * Set the theme mode.
   * @param darkMode True for dark mode. False for light mode.
   */
  private setMode(darkMode: boolean): void
  {
    if (darkMode)
    {
      this.renderer.removeClass(this.document.body, ThemeService.LIGHT_MODE_CSS_CLASS_NAME);
      this.setKendoUiControlsMode('kendoui-dark.css');
    }
    else
    {
      this.renderer.addClass(this.document.body, ThemeService.LIGHT_MODE_CSS_CLASS_NAME);
      this.setKendoUiControlsMode('kendoui-light.css');
    }
  }

  /** Apply the theme mode stored in the settings. */
  apply(): void
  {
    this.setMode(this.settings.darkMode);
  }

  /**
   * Observe and apply any future changes of user preferences of the theme mode through the operating system (dark or light).
   *
   * If the user decides to change the theme through the operating system, the changes will be immediately reflected in the application.
   *
   * @private
   */
  private startSystemModeSynchronization(): void
  {
    if ((!this.destructionSubject) && (this.document.defaultView))
    {
      this.destructionSubject = new Subject<void>();
      fromEvent(this.document.defaultView.matchMedia(ThemeService.SYSTEM_DARK_MODE_MEDIA_QUERY), 'change').pipe(
        takeUntil(this.destructionSubject)
      ).subscribe((e: Event) =>
      {
        const mediaqueryListEventMediaQueryListEvent: MediaQueryListEvent = e as MediaQueryListEvent;
        const darkMode = (mediaqueryListEventMediaQueryListEvent) ? mediaqueryListEventMediaQueryListEvent.matches : this.isSystemDark();
        this.setMode(darkMode);
      })
    }
  }

  /**
   * Stop observing and reflecting in the application the user's preferences about the theme in the operating system (dark or light).
   *
   * @private
   * @memberof ThemeService
   */
  private stopSystemModeSynchronization(): void
  {
    if (this.destructionSubject)
    {
      this.destructionSubject.next();
      this.destructionSubject.complete();
      this.destructionSubject = null;
    }
  }

  ngOnDestroy(): void {
    this.stopSystemModeSynchronization();
  }

  /** Apply the theme mode according to the user's operating system preferences. */
  setSystemMode(): void
  {
    const darkMode = this.isSystemDark();
    this.setMode(darkMode);
    this.startSystemModeSynchronization();
    this.removeLocalStorageItem(ThemeService.SETTINGS_KEY);
  }

  /**
   * Apply and persist in the configuration the theme mode chosen by the user within the application.
   * @param darkMode True for dark mode. False for light mode.
   */
  setUserDefinedMode(darkMode: boolean): void
  {
    this.setMode(darkMode);
    this.stopSystemModeSynchronization();
    this.settings.darkMode = darkMode;
    this.setLocalStorageItem(ThemeService.SETTINGS_KEY, this.settings);
  }

  //** Apply light mode. */
  setLightMode()
  {
    this.setUserDefinedMode(false);
  }

  /** Apply dark mode. */
  setDarkMode()
  {
    this.setUserDefinedMode(true);
  }
}

 

app.components.ts:

import { Component, OnInit } from '@angular/core';
import { products } from './products';
import { ThemeService } from './theme/theme.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  data!: any[];

  constructor(private themeService: ThemeService) {
  }

  ngOnInit(): void {
    this.themeService.apply();
    this.data = products;
  }

  setDarkModeTheme()
  {
    this.themeService.setDarkMode();
  }

  setLightModeTheme()
  {
    this.themeService.setLightMode();
  }

  setSystemModeTheme()
  {
    this.themeService.setSystemMode();
  }
}

 

app.component.html:

<div>
  <h1>Choose Theme</h1>

  <kendo-buttongroup selection="single" look="outline">
    <button kendoButton [toggleable]="true" (click)="setDarkModeTheme()">
      <span>
        Dark Mode Theme
      </span>
    </button>
    <button kendoButton [toggleable]="true" (click)="setLightModeTheme()">
      <span>
        Light Mode Theme
      </span>
    </button>
    <button kendoButton [toggleable]="true" (click)="setSystemModeTheme()">
      <span>
        Operating System Mode Theme
      </span>
    </button>
  </kendo-buttongroup>
</div>

<div>
  <h1>Grid example</h1>

  <kendo-grid [data]="data" [height]="410">
    <kendo-grid-column field="ProductID" title="ID" [width]="40">
    </kendo-grid-column>
    <kendo-grid-column field="ProductName" title="Name" [width]="250">
    </kendo-grid-column>
    <kendo-grid-column field="Category.CategoryName" title="Category">
    </kendo-grid-column>
    <kendo-grid-column field="UnitPrice" title="Price" [width]="80">
    </kendo-grid-column>
    <kendo-grid-column field="UnitsInStock" title="In stock" [width]="80">
    </kendo-grid-column>
    <kendo-grid-column field="Discontinued" title="Discontinued" [width]="120">
      <ng-template kendoGridCellTemplate let-dataItem>
        <input type="checkbox" [checked]="dataItem.Discontinued" disabled />
      </ng-template>
    </kendo-grid-column>
  </kendo-grid>
</div>

 

Tags
Styling / Themes
Asked by
Venkat
Top achievements
Rank 1
Answers by
garri
Top achievements
Rank 2
Iron
Iron
Iron
Share this question
or