Ionic Dark Mode - Force Light/Dark and System theme

I would like to add a toggle for my Ionic project that controls the theme of the entire application. The toggle can force a light mode/dark mode irrespective of what the system mode is (ie. stays dark mode even if the system turns from dark → light) and also an option to toggle back to automatic.

Currently what I have done is follow the Ionic Conference App https://github.com/ionic-team/ionic-conference-app and what I’ve done to my base project is:

  1. Remove @media (prefers-color-scheme: dark) from variables.scss and add .dark-theme class to the media queries, ie. body.dark-theme, .ios body.dark-theme.ios
  2. Add an ion-segment to my settings page:
    <ion-label position="fixed">Mode</ion-label>
    <ion-segment
      [(ngModel)]="modeService.mode"
      (ionChange)="modeService.setMode()"
    >
      <ion-segment-button value="light">
        <ion-label>Light</ion-label>
      </ion-segment-button>
      <ion-segment-button value="dark">
        <ion-label>Dark</ion-label>
      </ion-segment-button>
      <ion-segment-button value="auto">
        <ion-label>Auto</ion-label>
      </ion-segment-button>
    </ion-segment>
  1. in my modeService.ts file (which is the service I use to save the mode to the storage and handle the mode change:
dark: boolean;
  mode = 'auto';
  prefDark = window.matchMedia('(prefers-color-scheme: dark)');

  constructor() { }

  setMode = async (): Promise<void> => {
    const storeMode = this.mode;

    await Storage.set({
      key: 'mode',
      value: storeMode
    });

    if (this.mode !== 'auto') {
      this.dark = (this.mode === 'dark') ? true : false;
    } else {
      this.dark = this.prefDark.matches;
      this.prefDark.addEventListener('change', e => {
        this.dark = e.matches;
      });
    }
  };

  checkMode = async (): Promise<void> => {
    const { value } = await Storage.get({ key: 'mode' });
    if (value) {
      this.mode = value;
    }
  };
}
  1. In app.component.html I added [class.dark-theme]="modeService.dark" to <ion-app>
  2. In app.component.ts:
export class AppComponent implements OnInit{
  prefDark = window.matchMedia('(prefers-color-scheme: dark)');

  constructor(
    private themeService: ThemesService,
    public teamService: TeamService,
    private helpService: HelpService,
    public modeService: ModeService,
    private router: Router,
    public modalController: ModalController,
    private menuController: MenuController,
  ) { }

 async ngOnInit() {
    await ... /// some other theme change
    if (this.modeService.mode === 'auto') {
      this.modeService.dark = this.prefDark.matches;
      this.prefDark.addEventListener('change', e => {
        this.modeService.dark = e.matches;
      });
    }
  }

The problem is, the toggle work but say when I have the segment to dark and the OS theme is changed to dark, then I toggle the OS theme to light the app turns back to the light theme and does not keep the dark theme.

Fix as follows:

This is my file mode.service.ts

import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';

@Injectable({
  providedIn: 'root'
})
export class ModeService {
  dark: boolean;
  mode = 'auto';
  prefDark = window.matchMedia('(prefers-color-scheme: dark)');

  constructor() { }

  setMode = async (): Promise<void> => {
    const storeMode = this.mode;

    await Preferences.set({
      key: 'mode',
      value: storeMode
    });

    if (this.mode !== 'auto') {
      this.dark = (this.mode === 'dark') ? true : false;
    } else {
      this.dark = this.prefDark.matches;
      this.prefDark.addEventListener('change', e => {
        this.dark = e.matches;
      });
    }
  };

  checkMode = async () => {
    const { value } = await Preferences.get({ key: 'mode' });
    if (value) {
      this.mode = value;
    }

    return this.mode;
  };
}

In app.compontent.ts

this.modeService.checkMode().then((data) => {
      if (data === 'auto' || data === 'dark') {
        this.modeService.dark = this.prefDark.matches;
        this.prefDark.addEventListener('change', e => {
          this.modeService.dark = e.matches;
        });
      }
    }).catch((err) => console.log(err));