Theming System with Angular and CSS Custom Properties
March 14, 2021
In this quick tutorial we are going to build a theming system with Angular and CSS Custom Properties (Variables) and without any extra libraries. While we buid a Dark-/Light-Mode switch, the concept can be applied to any theming you wish.
If you don’t want to follow along, you can jump right ahead and check it out in the Stackblitz
Interfaces & classes
Let’s start of with our Interfaces, enums and classes.
export enum AvailableProperties {
Background = '--background',
FontColor = '--font-color',
}
export enum Theme {
Light = 'Light',
Dark = 'Dark',
}
export interface IThemeOptions {
name: Theme;
customProperties: Record<AvailableProperties, string>;
}
The IThemeOptions
will be used to create our classes that represent each available Theme later.
AvailableProperties
will contain all available custom properties and map them to a enum value, to be easier usable.
The enum Theme
is just a list that contains all available themes so we can reference them by enum value later.
Now we can create our theme classes.
import { AvailableProperties, IThemeOptions, Theme } from './Theme';
export const lightTheme: IThemeOptions = {
name: Theme.Light,
customProperties: {
[AvailableProperties.Background]: '#fefefe',
[AvailableProperties.FontColor]: '#24292e',
}
};
export const darkTheme: IThemeOptions = {
name: Theme.Dark,
customProperties: {
[AvailableProperties.Background]: '#263238',
[AvailableProperties.FontColor]: '#c9d1c9',
}
};
Directive
The directive is responsible to set the custom properties to the given element.
import { Directive, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { ThemeService } from './theme.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AvailableProperties, IThemeOptions } from './Theme';
@Directive({
selector: '[appTheme]'
})
export class ThemeDirective implements OnInit, OnDestroy {
private unsubscribe: Subject<boolean> = new Subject();
constructor(
private elementRef: ElementRef,
private themeService: ThemeService
) {}
public ngOnInit(): void {
const active: IThemeOptions = this.themeService.getActiveTheme();
if (active) {
this.updateTheme(active);
}
this.themeService.themeChange
.pipe(
takeUntil(this.unsubscribe)
)
.subscribe((theme: IThemeOptions) => this.updateTheme(theme));
}
public ngOnDestroy(): void {
this.unsubscribe.next();
}
public updateTheme(theme: IThemeOptions): void {
Object.keys(theme.customProperties).forEach((key: string): void => {
this.elementRef.nativeElement.style.setProperty(key, theme.customProperties[key as AvailableProperties]);
});
}
}
The most important part is the updateTheme
method. It takes a given Theme, iterates through all customProperties and applies all values to the given elementRef
of the directive.
The ngOnInit
sets the theme initially and also adds a listener if the theme is changed via our theme service (happens in the next step)
Service
The ThemeService
will be used to switch the Themes.
import { Injectable, EventEmitter } from '@angular/core';
import { lightTheme } from './light-theme';
import { darkTheme } from './dark-theme';
import { IThemeOptions, Theme } from './Theme';
@Injectable()
export class ThemeService {
public themeChange: EventEmitter<IThemeOptions> = new EventEmitter<IThemeOptions>();
private themes: Array<IThemeOptions> = [lightTheme, darkTheme];
private activeTheme: Theme = Theme.Light;
public getActiveTheme(): IThemeOptions {
const theme: IThemeOptions | undefined = this.themes.find((option: IThemeOptions) => option.name === this.activeTheme);
if (!theme) {
throw new Error(`Theme not found: '${this.activeTheme}'`);
}
return theme;
}
public setTheme(name: Theme): void {
this.activeTheme = name;
this.themeChange.emit( this.getActiveTheme());
}
}
The main part of this service is the themeChange
EventEmitter (which we listen to in our directive). The setTheme
function is used to change the theme. It updates the internal variable and emits a new value in the EventEmitter. The initial value of the activeTheme
variable is the theme that will be used initially.
Now add the ThemeService
and the ThemeDirective
to the respective angular.module and add it the service to the providers array and the directive to the declarations array.
Apply styles
Now most technical stuff is done and we can start implementing our styles. For this just add a class and start using your properties right away, for example
.content {
background-color: var(--background);
color: var(--font-color);
}
*Even though this is technically not needed I always add all available custom-properties to the main css file on the root-element.
:root {
--background: #f6f7f9
--font-color: #24292e
}
This helps your IDE to provide autocompletion if you reference the available custom properties later.
Theme Switching
Now we need button to switch the themes around. In this example let’s build a quick darkmode switch.
<button matButton (click)="toggleTheme()">Toggle Theme</button>
public isLightThemeActive: boolean = true;
constructor(private themeService: ThemeService) {}
public toggleTheme(): void {
this.isLightThemeActive = !this.isLightThemeActive;
if (this.isLightThemeActive) {
this.themeService.setTheme(Theme.Light);
} else {
this.themeService.setTheme(Theme.Dark);
}
}
Now everything is complete and should work properly.
Extend functionality
I’d suggest to add a small transition style, so the theme switching progress looks more smooth, for example something like this
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
If you need more properties just add the new value to the enum AvailableProperties
and implement the respective values in the theme classes.
Conclusion
With just one service, one directive and couple of classes/interface we now have a complete theming system. And while we build a dark-/light-mode switch in this tutorial, this can easily be adapted to green and blue theme or whatever you like - and is not limited to just having 2 themes. You can add as many classes as you like.
Feel free to play around with the Stackblitz and let me know if this was helpful for you.
Personal Blog written by Nicolas Gehlert, software developer from Freiburg im Breisgau. Developer & Papa. Github | Twitter
Add a comment
Comments
Nico
May 11, 2021
vgon
May 10, 2021
Hi Nicolas, I tried to import your example into my project and use appTheme in the html of one of my components (only app.component.html seem to work properly) but the —background or —font-color are displayed as undefined when checking their value through DOM inspection. Thank you in advance
Hi Vgon, Can you try to recreate your current example in a Stackblitz? There is probably just a minor thing missing somewhere. Happy to help if you provide the example :) Kind regards Nico