When users use my Angular apps, they shall be able to select a theme from a theme list, some of which are dark.
If you google JavaScript theme loader, you may find many articles and example codes. And Google AI and Copilot and alike may generate fairly decent codes, as many JavaScript programmers have crafted many for over 2 decades.
I have crafted one from scratch based on specific functional requirements and technical requirements, conforming to my design principles for UI, UX and Developer Experience.
As a web app user, I want to choose from multiple available themes — sometimes light, other times dark.
Develop a TypeScript-based API that provides helper functions or classes for building a theme picker in web applications. The API should be framework‑agnostic, while optionally offering convenient integration points for Angular applications.
colors.css, with an optional dark‑mode variant like colors-dark.css.** If you find your requirements match mine, please read on.**
The following sourcecode is crafted in TypeScript for Angular SPA, and it should be easy to use in other Web apps or sites crafted in JavaScript, with little modification.
## Theme Loader
themeLoader.ts (full sourcecode)
export class ThemeLoader {
private static readonly key = 'app.theme'; //the key for storing selected theme filename. Generally no need to change
private static readonly themeLinkId = 'theme';
private static readonly appColorsLinkId = 'app-colors';
private static readonly colorsCss = 'colors.css';
private static readonly colorsDarkCss = 'colors-dark.css'; // if your app use light only or dark only, just make colorsCss and colorsDarkCss the same filename.
/**
* selected theme file name saved in localStorage.
*/
static get selectedTheme(): string | null {
return localStorage.getItem(this.key);
};
private static set selectedTheme(v: string) {
localStorage.setItem(this.key, v);
};
/**
*
* @param picked one of the prebuilt themes, typically used with the app's theme picker.
* or null for the first one in themesDic, typically used before calling `bootstrapApplication()`.
* @param appColorsDir if the app is using prebuilt theme only for all color styling, this parameter could be ignore.
* Otherwise, null means that colors.css or colors-dark.css is in the root,
* or a value like 'conf/' is for the directory under root,
* or undefined means the app uses theme only for color.
*/
static loadTheme(picked: string | null, appColorsDir?: string | null) {
if (!AppConfigConstants.themesDic || Object.keys(AppConfigConstants.themesDic).length === 0) {
console.error('Need AppConfigConstants.themesDic with at least 1 item');
return;
}
let themeLink = document.getElementById(this.themeLinkId) as HTMLLinkElement;
if (themeLink) { // app has been loaded in the browser page/tab.
const currentTheme = themeLink.href.substring(themeLink.href.lastIndexOf('/') + 1);
const notToLoad = picked == currentTheme;
if (notToLoad) {
return;
}
const r = AppConfigConstants.themesDic[picked!];
if (!r) {
return;
}
themeLink.href = picked!;
this.selectedTheme = picked!;
console.info(`theme altered to ${picked}.`);
if (appColorsDir === undefined) {
return;
}
let appColorsLink = document.getElementById(this.appColorsLinkId) as HTMLLinkElement;
if (appColorsLink) {
const customFile = r.dark ? this.colorsDarkCss : this.colorsCss;
appColorsLink.href = (appColorsDir === null) ? customFile : appColorsDir + customFile;
}
} else { // app loaded for the first time, then create
themeLink = document.createElement('link');
themeLink.id = this.themeLinkId;
themeLink.rel = 'stylesheet';
const firstTheme = picked ?? Object.keys(AppConfigConstants.themesDic!)[0];
themeLink.href = firstTheme;
document.head.appendChild(themeLink);
this.selectedTheme = firstTheme;
console.info(`Initially loaded theme ${firstTheme}`);
if (appColorsDir === undefined) {
return;
}
const appColorsLink = document.createElement('link');
appColorsLink.id = this.appColorsLinkId;
appColorsLink.rel = 'stylesheet';
const customFile = AppConfigConstants.themesDic[firstTheme].dark ? this.colorsDarkCss : this.colorsCss;
appColorsLink.href = (appColorsDir === null) ? customFile : appColorsDir + customFile;
document.head.appendChild(appColorsLink);
}
}
}
Typically an Web app with JavaScript has some settings that should be loaded at the very beginning synchronously.
Data schema (full sourcecode):
export interface ThemeDef {
/** Relative path or URL to CDN */
filePath: string;
/** Display name */
display?: string;
/** Dark them or not */
dark?: boolean;
}
export interface ThemesDic {
[filePath: string]: {
display?: string,
dark?: boolean
}
}
siteconfig.js:
const SITE_CONFIG = {
themesDic: {
"assets/themes/rose-red.css":{name: "Roes & Red", dark:false},
"assets/themes/azure-blue.css":{name: "Azure & Blue", dark:false},
"assets/themes/magenta-violet.css":{name: "Magenta & Violet", dark:true},
"assets/themes/cyan-orange.css":{name: "Cyan & Orange", dark:true}
}
}
Hints:
index.html (full sourcecode):
...
<body>
<script src="conf/siteconfig.js"></script>
...
To ensure Angular runtime to utilize the theme as early as possible before rendering any component, ThemeLoader must be called before bootstrap:
main.ts
ThemeLoader.loadTheme(ThemeLoader.selectedTheme, 'conf/');
bootstrapApplication(AppComponent, appConfig);
Typically the UI of switching between themes is a dropdown implemented using something like MatMenu or MatSelect, while there are Websites for graphic designers coming with complex runtime styles and theme selection UI, like what in PrimeVue. However, I would doubt any business app or consumer app would favor such powerful complexity.


HTML with MatSelect:
<mat-form-field>
<mat-label i18n>Themes</mat-label>
<mat-select #themeSelect (selectionChange)="themeSelectionChang($event)" [value]="currentTheme">
@for (item of themes; track $index) {
<mat-option [value]="item.fileName"></mat-option>
}
</mat-select>
</mat-form-field>
Code behind (full codes):
themes?: ThemeDef[];
get currentTheme() {
return ThemeLoader.selectedTheme;
}
...
this.themes = AppConfigConstants.themesDic ? Object.keys(AppConfigConstants.themesDic).map(k => {
const c = AppConfigConstants.themesDic![k];
const obj: ThemeDef = {
name: c.name,
fileName: k,
dark: c.dark
};
return obj;
}) : undefined;
...
themeSelectionChang(e: MatSelectChange) {
ThemeLoader.loadTheme(e.value, 'conf/');
}
The API exposes 3 contracts:
static loadTheme(picked: string | null, appColorsDir?: string | null) of themeLoader to be called during startup, and when the app user picks one from available themes.static get selectedTheme(): string | null of themeLoader.themeDef.ts for the themes dictionary in siteconfig.js, along with environment.common.ts for strongly typed site config during Web app startup.ThemeLoader.loadTheme() before the bootstrap of the Web app.ThemeLoader.loadTheme() when the picker picks a theme.siteconfig.js.<script src="conf/siteconfig.js"></script> .Remarks:
themeDef.ts and environment.common.ts won’t be built into JavaScript, therefore they are not part of the APIAfter Angular Material Components v12, the documentation site has been merged into the components’ repository.
Please check https://github.com/angular/components/blob/main/docs/src/app/shared/theme-picker/ and https://github.com/angular/material.angular.io/blob/main/src/app/shared/style-manager/ .
The design basically conforms to the “Requirements” above, though more complex and comprehensive in the contexts of the documentation site, and within its business scope. Overall, decent and elegant enough.
And likely, the design and the implementation have inspired many LLMs based AI code generators.
Using the requirements above as prompt, I asked Windows Copilot to generate sourcecode, then asked M365 Copilot of another account, and the Claude.AI etc.
For almost a year, since early 2025, I have been using Windows Copilot and M365 Copilot to help my daily programming works, mostly trivial works, and occasionally heavy scaffolding, covering these areas:
I feel pleased, relax and productive with such junior programmer helping me, releasing me from trivial and repetitive technical details.
The attempts above asking AI to generate a theme loader is to have more hand-on experience in using AI in other areas. I will be writing a series of articles about how AI could help senior developers, the inherent shortfalls of AI code generators and why such short falls exist.