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, therefore give code snippets for Angular app and React app using this API.
colors.css, with an optional dark‑mode variant like colors-dark.css.Using the requirements as prompt, I asked ChatGPT 4 to generate sourcecode, and the following is the result.
Below is a framework-agnostic, TypeScript theme-management API plus Angular and React usage examples. The design keeps core theme logic separate from UI, supports multiple light/dark themes, avoids startup flicker, works offline/PWA, and allows post-deployment adjustments via config and CSS files.
Key ideas
<link> tags, persists selection, syncs across tabs./assets/themes/themes.config.json
{
"defaultThemeId": "light",
"themes": [
{
"id": "light",
"label": "Light",
"mode": "light",
"styles": [
"/assets/themes/base-light.css",
"/assets/themes/colors.css"
]
},
{
"id": "dark",
"label": "Dark",
"mode": "dark",
"styles": [
"/assets/themes/base-dark.css",
"/assets/themes/colors-dark.css"
]
},
{
"id": "high-contrast",
"label": "High Contrast",
"mode": "dark",
"styles": [
"https://cdn.example.com/themes/high-contrast.css"
]
}
]
}
export type ThemeMode = 'light' | 'dark';
export interface ThemeDefinition {
id: string;
label: string;
mode: ThemeMode;
styles: string[]; // URLs to CSS files
}
export interface ThemeConfig {
defaultThemeId: string;
themes: ThemeDefinition[];
}
export class ThemeManager {
private config!: ThemeConfig;
private activeThemeId!: string;
private readonly storageKey = 'app.theme';
private readonly channel = 'app-theme-sync';
private bc?: BroadcastChannel;
constructor(private readonly doc: Document = document) {}
async init(configUrl: string): Promise<void> {
this.config = await fetch(configUrl).then(r => r.json());
const stored =
localStorage.getItem(this.storageKey) || this.config.defaultThemeId;
this.applyTheme(stored, { persist: false });
// cross-tab sync
if ('BroadcastChannel' in window) {
this.bc = new BroadcastChannel(this.channel);
this.bc.onmessage = e => this.applyTheme(e.data, { persist: false });
} else {
window.addEventListener('storage', e => {
if (e.key === this.storageKey && e.newValue) {
this.applyTheme(e.newValue, { persist: false });
}
});
}
}
getThemes(): ThemeDefinition[] {
return this.config.themes;
}
getActiveTheme(): string {
return this.activeThemeId;
}
applyTheme(
themeId: string,
opts: { persist?: boolean } = { persist: true }
): void {
if (themeId === this.activeThemeId) return;
const theme = this.config.themes.find(t => t.id === themeId);
if (!theme) return;
this.loadStyles(theme.styles);
this.doc.documentElement.dataset.theme = theme.id;
this.doc.documentElement.dataset.themeMode = theme.mode;
this.activeThemeId = themeId;
if (opts.persist !== false) {
localStorage.setItem(this.storageKey, themeId);
this.bc?.postMessage(themeId);
}
}
private loadStyles(urls: string[]): void {
const head = this.doc.head;
// Mark existing theme styles
const existing = Array.from(
head.querySelectorAll<HTMLLinkElement>('link[data-theme-style]')
);
// Load only missing styles
urls.forEach(url => {
if (existing.some(l => l.href === new URL(url, location.href).href)) {
return;
}
const link = this.doc.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.dataset.themeStyle = 'true';
head.appendChild(link);
});
// Remove styles no longer used
existing.forEach(l => {
if (!urls.includes(l.getAttribute('href') || '')) {
head.removeChild(l);
}
});
}
}
Why this meets the requirements
Add this tiny inline script to index.html before any CSS/framework scripts:
<script>
(function () {
try {
var theme = localStorage.getItem('app.theme');
if (theme) {
document.documentElement.dataset.theme = theme;
}
} catch {}
})();
</script>
This ensures the correct theme is applied before first paint.
// theme.provider.ts
import { APP_INITIALIZER, Provider } from '@angular/core';
import { ThemeManager } from './theme-manager';
export const themeManager = new ThemeManager();
export const ThemeInitializer: Provider = {
provide: APP_INITIALIZER,
multi: true,
useFactory: () => () =>
themeManager.init('/assets/themes/themes.config.json')
};
// app.module.ts
@NgModule({
providers: [ThemeInitializer]
})
export class AppModule {}
@Component({
selector: 'app-theme-picker',
template: `
<select
[value]="active"
(change)="setTheme($event.target.value)">
<option *ngFor="let t of themes" [value]="t.id">
</option>
</select>
`
})
export class ThemePickerComponent {
themes = themeManager.getThemes();
active = themeManager.getActiveTheme();
setTheme(id: string) {
themeManager.applyTheme(id);
this.active = id;
}
}
// theme.ts
import { ThemeManager } from './theme-manager';
export const themeManager = new ThemeManager();
export async function initTheme() {
await themeManager.init('/assets/themes/themes.config.json');
}
// main.tsx
await initTheme();
createRoot(document.getElementById('root')!).render(<App />);
export function ThemePicker() {
const [themes] = React.useState(themeManager.getThemes());
const [active, setActive] = React.useState(themeManager.getActiveTheme());
const onChange = (id: string) => {
themeManager.applyTheme(id);
setActive(id);
};
return (
<select value={active} onChange={e => onChange(e.target.value)}>
{themes.map(t => (
<option key={t.id} value={t.id}>
{t.label}
</option>
))}
</select>
);
}
themes.config.json and CSS files via service workerThis API:
If you want, I can also:
Comparing with what generated by Windows/M365 Copilot, Claude AI and Google AI Studio, ChatGTP generates the shortest sourcecode and the simplest design.
Common Pitfalls:
Regarding the code snippets for Angular usage, the entry is too late. Startup logic must be loaded before the bootstrap.
Regarding the code snippets for React usage, the entry looks OK. Though a little flicking may be expected, at least it won’t be as severe as the Angular app.
I have crafted one from scratch based on the same functional requirements and technical requirements, conforming to my design principles for UI, UX and Developer Experience: