Introduction: Crafting a Flexible White-Label SaaS UI

Welcome to Chapter 15! In the previous chapters, we’ve explored various advanced Angular concepts, from efficient routing to state management and performance optimization. Now, it’s time to apply that knowledge to a truly exciting and practical challenge: building a White-Label Software as a Service (SaaS) User Interface.

A white-label application is a generic product or service that can be rebranded by other companies to make it appear as their own. For a UI, this means creating a single codebase that can be dynamically customized—colors, logos, text, and even features—to match multiple client brands. This chapter will guide you through the architectural considerations and implementation details required to build such a flexible and scalable Angular application. We’ll focus on how to manage dynamic styling, configuration, and tenant-specific features, ensuring a robust and maintainable system.

By the end of this project, you’ll not only have a deeper understanding of Angular’s capabilities but also gain invaluable insights into designing complex, multi-tenant frontend systems. We’ll leverage modern Angular features, including standalone components and CSS custom properties, to achieve our goals.

Core Concepts: The Anatomy of a White-Label UI

Designing a white-label SaaS UI is more than just changing a logo; it’s about building a highly adaptable system. Let’s break down the core concepts that make this possible.

What is White-Labeling in SaaS?

Imagine you’ve built an amazing analytics dashboard. Instead of selling it directly as “YourCompany Analytics,” you want to offer it to other businesses (let’s call them “tenants”) who can then put their brand on it and offer it to their customers. This is white-labeling. From a UI perspective, it means:

  • Dynamic Theming: Changing colors, fonts, and potentially layout to match a tenant’s brand guidelines.
  • Tenant-Specific Assets: Displaying a tenant’s logo, favicons, and other brand imagery.
  • Configuration Overrides: Adjusting features, navigation items, or even text labels based on the active tenant.
  • Data Isolation: While primarily a backend concern, the UI must correctly interact with tenant-isolated data.

Why is this important? It allows a single product to serve a broad market, reducing development and maintenance costs compared to building separate applications for each client.

Key Architectural Challenges

Building a white-label UI introduces specific architectural challenges:

  1. Dynamic Theming: How do you apply different styles without duplicating CSS or resorting to complex, fragile solutions? CSS Custom Properties (variables) are our best friend here.
  2. Tenant Identification: How does the application know which tenant is currently accessing it? This often involves URL subdomains (e.g., tenant1.your-saas.com), URL paths (e.g., your-saas.com/tenant1), or specific headers/query parameters.
  3. Configuration Management: How do you load tenant-specific settings (API endpoints, feature flags, text content) efficiently and securely?
  4. Asset Loading: How do you dynamically load logos and other images that differ per tenant?
  5. Maintainability: Ensuring that adding a new tenant doesn’t require a new build or extensive code changes.

Multi-Tenancy Strategies: Build-time vs. Runtime

There are two primary approaches to handling multi-tenancy in the frontend:

1. Build-Time Multi-Tenancy (Less Flexible)

In this approach, you might have separate build configurations for each tenant. When you build the application, you generate a distinct bundle for tenantA and another for tenantB, each pre-packaged with their specific assets and configurations.

Pros:

  • Simpler initial setup for small number of tenants.
  • Potentially smaller bundles if features are stripped out per tenant.

Cons:

  • Scalability Nightmare: As the number of tenants grows, managing separate builds becomes unsustainable. Deployments become complex.
  • Maintenance Overhead: Bug fixes require rebuilding and redeploying all tenant-specific applications.
  • No new tenants without a new build.

2. Runtime Multi-Tenancy (Our Focus!)

This is the more robust and scalable approach. You build one universal application bundle. When a user accesses the application, it determines the tenant at runtime (e.g., from the URL) and then dynamically loads the appropriate theme, configuration, and assets.

Pros:

  • Single Build & Deployment: One codebase, one build, one deployment artifact. Dramatically reduces CI/CD complexity.
  • Scalability: Easily onboard new tenants by simply adding their configuration data. No code changes or redeployments needed.
  • Dynamic Customization: Full flexibility to change themes and configurations without rebuilding.

Cons:

  • Requires careful design for dynamic loading.
  • Initial application bundle might be slightly larger if it contains all potential features (though modern Angular’s tree-shaking helps).

We will focus on the runtime multi-tenancy approach for our white-label UI.

Architectural Overview for Runtime Multi-Tenancy

Let’s visualize the flow for our runtime white-label SaaS UI.

flowchart TD User(User) --> A[Browser Request: tenantX.saas.com] A --> B{Web Server / CDN} B --> C[Serve Universal Angular App Bundle] C --> D[Angular App Bootstraps] D --> E[Identify Tenant from Hostname] E --> F[Load Tenant-Specific Configuration & Theme] F --> G[Apply Theme & Configuration] G --> H[Render UI with Tenant Branding] H --> User

Explanation of the Flow:

  1. User Request: A user navigates to a tenant-specific URL, like customerA.my-saas.com.
  2. Web Server / CDN: The web server (or CDN) receives the request and serves the same universal Angular application bundle to all tenants.
  3. Angular App Bootstraps: The Angular application starts up in the user’s browser.
  4. Identify Tenant: Crucially, one of the first things the application does is identify the current tenant. This is often done by inspecting the window.location.hostname.
  5. Load Tenant-Specific Configuration & Theme: Based on the identified tenant, the application makes a request to a backend API or loads a specific JSON file to fetch the tenant’s branding details (colors, logo URL, feature flags, etc.). This is a critical step that happens before the main UI components are rendered.
  6. Apply Theme & Configuration: The fetched data is then used to dynamically apply CSS custom properties (for theming) and configure services (for feature flags or routing).
  7. Render UI: Finally, the Angular application renders its components, which now automatically pick up the tenant-specific styles and configurations, presenting a fully branded experience.

This architecture ensures that our application is highly flexible and scalable, allowing us to onboard new tenants with minimal operational overhead.

Step-by-Step Implementation: Building Our White-Label UI

Let’s get our hands dirty and start building this white-label application. We’ll use Angular v17.3.0 (or the latest stable by Feb 2026, which would likely be v18 or v19, but the principles remain the same) with standalone components.

Step 1: Initialize the Angular Project

First, ensure you have the Angular CLI installed. If not, install it globally:

npm install -g @angular/cli@17.3.0 # or latest stable for Angular 17/18

Now, let’s create a new Angular project. We’ll call it white-label-saas.

ng new white-label-saas --standalone --style=scss --routing=true
cd white-label-saas

When prompted for the initial setup, choose Yes for routing and SCSS for styling. Standalone components are the default for new projects in Angular 17+.

Step 2: Define Tenant Configuration Structure

We need a way to store tenant-specific data. For simplicity, we’ll start with a local assets/config folder containing JSON files. In a real application, this would come from a backend API.

Create a folder src/assets/config and add two files: tenant-a.json and tenant-b.json.

src/assets/config/tenant-a.json

{
  "tenantId": "tenant-a",
  "appName": "Awesome Analytics",
  "logoUrl": "/assets/logos/logo-tenant-a.png",
  "theme": {
    "primaryColor": "#007bff",
    "secondaryColor": "#6c757d",
    "fontFamily": "Roboto, sans-serif"
  },
  "features": {
    "advancedReporting": true,
    "userManagement": true
  }
}

src/assets/config/tenant-b.json

{
  "tenantId": "tenant-b",
  "appName": "Super Dashboard",
  "logoUrl": "/assets/logos/logo-tenant-b.png",
  "theme": {
    "primaryColor": "#28a745",
    "secondaryColor": "#ffc107",
    "fontFamily": "Open Sans, sans-serif"
  },
  "features": {
    "advancedReporting": false,
    "userManagement": true
  }
}

We also need some dummy logos. Create src/assets/logos and add two placeholder images named logo-tenant-a.png and logo-tenant-b.png. You can use any small image files for this.

Step 3: Create a Tenant Configuration Service

This service will be responsible for loading and providing the current tenant’s configuration.

Generate the service:

ng generate service services/tenant-config

Open src/app/services/tenant-config.service.ts and update it:

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, firstValueFrom } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

export interface TenantConfig {
  tenantId: string;
  appName: string;
  logoUrl: string;
  theme: {
    primaryColor: string;
    secondaryColor: string;
    fontFamily: string;
  };
  features: {
    advancedReporting: boolean;
    userManagement: boolean;
  };
}

@Injectable({
  providedIn: 'root'
})
export class TenantConfigService {
  private http = inject(HttpClient);
  private _currentTenantConfig = new BehaviorSubject<TenantConfig | null>(null);
  public readonly currentTenantConfig$ = this._currentTenantConfig.asObservable();

  constructor() { }

  /**
   * Initializes the tenant configuration based on the hostname.
   * This method should be called very early in the application lifecycle.
   */
  async initializeConfig(): Promise<TenantConfig> {
    const hostname = window.location.hostname;
    let tenantIdentifier: string;

    // Simple tenant identification based on subdomain
    // For example: tenant-a.localhost:4200 -> tenant-a
    // Or: localhost:4200/tenant-a -> tenant-a (would need server rewrite or different routing)
    if (hostname.includes('.')) {
      tenantIdentifier = hostname.split('.')[0];
    } else {
      // Fallback for local development or direct IP access
      // You might prompt the user or default to a specific tenant
      console.warn('Could not determine tenant from hostname. Defaulting to "tenant-a".');
      tenantIdentifier = 'tenant-a'; // Default for local development
    }

    const configUrl = `/assets/config/${tenantIdentifier}.json`;

    try {
      const config = await firstValueFrom(
        this.http.get<TenantConfig>(configUrl).pipe(
          tap(cfg => console.log('Loaded tenant config:', cfg)),
          catchError(error => {
            console.error(`Failed to load config for tenant "${tenantIdentifier}". Falling back to default.`, error);
            // In a real app, you might load a generic default config or redirect
            return firstValueFrom(this.http.get<TenantConfig>('/assets/config/tenant-a.json'));
          })
        )
      );
      this._currentTenantConfig.next(config);
      return config;
    } catch (error) {
      console.error('Error during initial tenant config load, even fallback failed.', error);
      // Handle critical failure: e.g., redirect to an error page
      throw error;
    }
  }

  getTenantConfig(): TenantConfig | null {
    return this._currentTenantConfig.getValue();
  }
}

Explanation:

  • We define a TenantConfig interface to ensure type safety.
  • HttpClient is injected to fetch JSON configuration files. Remember to import HttpClientModule in app.config.ts.
  • _currentTenantConfig is a BehaviorSubject to hold the current tenant’s configuration, allowing components to subscribe to changes.
  • initializeConfig() is an async method that determines the tenant from the hostname.
    • Why hostname? Subdomains are a common and robust way to identify tenants in production SaaS applications. tenant-a.your-saas.com clearly indicates tenant-a.
    • We include a fallback for local development (e.g., localhost:4200) to tenant-a.json.
    • It uses firstValueFrom to convert the Observable to a Promise, making it easier to use with async/await for initial setup.
    • Error handling is included to fall back to a default config if a specific tenant’s config isn’t found.
  • getTenantConfig() provides immediate access to the current config.

Step 4: Initialize Tenant Configuration at Application Startup

For our application to be truly white-labeled, the tenant configuration needs to be loaded before any major components render. Angular’s APP_INITIALIZER token is perfect for this.

Open src/app/app.config.ts and update it:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http'; // Import provideHttpClient
import { APP_INITIALIZER } from '@angular/core';
import { TenantConfigService } from './services/tenant-config.service';

function initializeApp(tenantConfigService: TenantConfigService) {
  return () => tenantConfigService.initializeConfig();
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(), // Provide HttpClient for the application
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [TenantConfigService],
      multi: true // Essential for APP_INITIALIZER
    }
  ]
};

Explanation:

  • provideHttpClient() is added to appConfig.providers to make HttpClient available.
  • We define an initializeApp function that takes TenantConfigService and returns a function that calls tenantConfigService.initializeConfig().
  • APP_INITIALIZER is a special injection token that allows you to run code before the application fully bootstraps.
    • Why APP_INITIALIZER? This ensures that our TenantConfigService has loaded the correct tenant configuration before any component tries to use it. Without this, components might render with default values briefly, leading to a “flash” of incorrect branding.
    • useFactory points to our initializeApp function.
    • deps specifies the dependencies for initializeApp (our TenantConfigService).
    • multi: true means that multiple functions can be registered for APP_INITIALIZER.

Step 5: Implement Dynamic Theming with CSS Custom Properties

Now that we have our tenant configuration, let’s use it for dynamic theming.

Open src/index.html and add a placeholder for our theme CSS variables in the <body> tag. We’ll set these dynamically.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>WhiteLabelSaas</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body data-app-theme> <!-- Added data-app-theme attribute -->
  <app-root></app-root>
</body>
</html>

Next, in src/app/app.component.ts, we’ll inject the TenantConfigService and apply the theme.

import { Component, OnInit, OnDestroy, inject, Renderer2 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { TenantConfigService, TenantConfig } from './services/tenant-config.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  template: `
    <header class="app-header">
      <img [src]="logoUrl" alt="{{ appName }} Logo" class="app-logo">
      <h1>Welcome to {{ appName }}</h1>
    </header>
    <main class="app-main">
      <router-outlet></router-outlet>
    </main>
    <footer class="app-footer">
      <p>&copy; 2026 {{ appName }}. All rights reserved.</p>
    </footer>
  `,
  styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {
  title = 'white-label-saas';
  appName: string = 'Loading App...';
  logoUrl: string = '';

  private tenantConfigService = inject(TenantConfigService);
  private renderer = inject(Renderer2);
  private configSubscription: Subscription | undefined;

  ngOnInit(): void {
    this.configSubscription = this.tenantConfigService.currentTenantConfig$.subscribe(config => {
      if (config) {
        this.appName = config.appName;
        this.logoUrl = config.logoUrl;
        this.applyTheme(config.theme);
      }
    });
  }

  ngOnDestroy(): void {
    this.configSubscription?.unsubscribe();
  }

  private applyTheme(theme: TenantConfig['theme']): void {
    const body = this.renderer.selectRootElement('body');
    this.renderer.setStyle(body, '--primary-color', theme.primaryColor);
    this.renderer.setStyle(body, '--secondary-color', theme.secondaryColor);
    this.renderer.setStyle(body, '--font-family', theme.fontFamily);
  }
}

Explanation:

  • AppComponent now subscribes to currentTenantConfig$ to react to config changes (though for APP_INITIALIZER, it’s usually set once).
  • appName and logoUrl are bound to the template.
  • The applyTheme method uses Angular’s Renderer2 to set CSS custom properties on the <body> element.
    • Why Renderer2? It’s a safe way to interact with the DOM, especially important for server-side rendering (SSR) or web workers, as it abstracts direct DOM manipulation.
    • Why <body>? Setting variables on <body> makes them globally available to all elements within the application.

Now, let’s define our global SCSS variables in src/styles.scss and use them in app.component.scss.

src/styles.scss

/* You can import global styles here */

/* Default values for CSS custom properties */
body {
  --primary-color: #007bff; // Default primary
  --secondary-color: #6c757d; // Default secondary
  --font-family: Arial, sans-serif; // Default font
}

body {
  font-family: var(--font-family);
  margin: 0;
  color: #333;
  background-color: #f8f9fa;
}

h1, h2, h3, h4, h5, h6 {
  color: var(--primary-color);
}

// Example of using the variables in a component or global style
button {
  background-color: var(--primary-color);
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 5px;
  cursor: pointer;
  &:hover {
    background-color: var(--secondary-color);
  }
}

src/app/app.component.scss

.app-header {
  background-color: var(--primary-color); // Use the variable
  color: white;
  padding: 1rem 2rem;
  display: flex;
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  .app-logo {
    height: 40px;
    margin-right: 1rem;
  }

  h1 {
    margin: 0;
    font-size: 1.8rem;
    color: white; // Override to white for header
  }
}

.app-main {
  padding: 2rem;
  min-height: calc(100vh - 120px); // Adjust based on header/footer height
}

.app-footer {
  background-color: #343a40;
  color: #f8f9fa;
  padding: 1rem 2rem;
  text-align: center;
  font-size: 0.9rem;
}

Why CSS Custom Properties?

  • True Dynamic Theming: They allow us to change values at runtime using JavaScript, which then cascade throughout the CSS without recompiling or injecting new stylesheets.
  • Maintainability: All styling logic remains in CSS, referencing variables. No need for complex JavaScript to manipulate individual style properties.
  • Performance: The browser handles updates efficiently.

Step 6: Create a Tenant-Aware Home Component

Let’s create a simple home component that uses the tenant configuration.

ng generate component home --standalone --skip-tests

Open src/app/home/home.component.ts:

import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TenantConfigService, TenantConfig } from '../services/tenant-config.service';

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="home-container">
      <h2>Welcome, {{ currentTenant?.appName }} User!</h2>
      <p>This is your personalized dashboard for {{ currentTenant?.appName }}.</p>

      <h3>Features available to you:</h3>
      <ul>
        <li *ngIf="currentTenant?.features.advancedReporting">Advanced Reporting</li>
        <li *ngIf="currentTenant?.features.userManagement">User Management</li>
        <li *ngIf="!currentTenant?.features.advancedReporting">Basic Reporting (Advanced Reporting not enabled)</li>
      </ul>

      <button>Explore Dashboard</button>
    </div>
  `,
  styleUrl: './home.component.scss'
})
export class HomeComponent implements OnInit {
  currentTenant: TenantConfig | null = null;
  private tenantConfigService = inject(TenantConfigService);

  ngOnInit(): void {
    // Since APP_INITIALIZER ensures config is loaded, we can get it directly
    this.currentTenant = this.tenantConfigService.getTenantConfig();
  }
}

src/app/home/home.component.scss

.home-container {
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

  h2 {
    color: var(--primary-color); // Uses the tenant's primary color
    margin-bottom: 15px;
  }

  p {
    line-height: 1.6;
    margin-bottom: 20px;
  }

  ul {
    list-style-type: none;
    padding: 0;
    margin-bottom: 20px;

    li {
      background-color: #f0f0f0;
      padding: 8px 12px;
      margin-bottom: 5px;
      border-left: 3px solid var(--secondary-color); // Uses tenant's secondary color
    }
  }
}

Step 7: Configure Routing

Update src/app/app.routes.ts to include our HomeComponent.

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

export const routes: Routes = [
  { path: '', component: HomeComponent, title: 'Home' },
  { path: '**', redirectTo: '' } // Redirect any unknown paths to home
];

Step 8: Run and Test the Application

Now it’s time to see our white-labeling in action!

  1. Start the Angular development server:

    ng serve
    
  2. Test Tenant A: Open your browser and navigate to tenant-a.localhost:4200.

    • How? You’ll need to edit your system’s hosts file to map tenant-a.localhost to 127.0.0.1.
      • macOS/Linux: sudo nano /etc/hosts
      • Windows: notepad C:\Windows\System32\drivers\etc\hosts (run as Administrator) Add the following lines:
      127.0.0.1 tenant-a.localhost
      127.0.0.1 tenant-b.localhost
      
      Save the file.

    You should see “Awesome Analytics” with a blue theme and “Advanced Reporting” enabled.

  3. Test Tenant B: In the same browser, navigate to tenant-b.localhost:4200. You should now see “Super Dashboard” with a green/yellow theme and “Advanced Reporting” disabled.

This demonstrates the power of runtime multi-tenancy! A single build, dynamically themed and configured based on the URL.

Mini-Challenge: Extend White-Labeling

Challenge: Add a new configurable element to your white-label setup.

  1. Add a footerText property to your tenant-a.json and tenant-b.json configuration files. Make them different.
  2. Update the TenantConfig interface in tenant-config.service.ts to include footerText.
  3. Modify app.component.ts to bind the footerText to the footer section of the template.
  4. Verify that when you switch between tenant-a.localhost:4200 and tenant-b.localhost:4200, the footer text updates accordingly.

Hint: Remember to update the footer element in your app.component.html (or inline template).

What to observe/learn: This exercise reinforces how easy it is to extend the dynamic configuration to new UI elements without changing core application logic, showcasing the flexibility of this architectural pattern.

Common Pitfalls & Troubleshooting

Building flexible systems can introduce new challenges. Here are some common pitfalls and how to troubleshoot them:

  1. Configuration Not Loading:

    • Symptom: Your app defaults to tenant-a even when navigating to tenant-b.localhost.
    • Check:
      • Is provideHttpClient() included in app.config.ts?
      • Are your tenant-a.json and tenant-b.json files correctly placed in src/assets/config?
      • Are there any network errors in your browser’s developer console when fetching /assets/config/tenant-b.json?
      • Did you correctly map tenant-b.localhost to 127.0.0.1 in your hosts file?
      • Is APP_INITIALIZER correctly configured with multi: true?
  2. Theming Not Applying:

    • Symptom: Colors or fonts don’t change, or only partially change.
    • Check:
      • Are the CSS custom properties (e.g., --primary-color) actually being set on the <body> element? Inspect the <body> element in your browser’s dev tools and look at its computed styles.
      • Are your SCSS files correctly referencing these custom properties using var(--primary-color)?
      • Are there any CSS specificity issues? A more specific CSS rule might be overriding your custom property. Try making your rules more specific or using !important temporarily for debugging (but avoid in production).
  3. Local Development Hostname Issues:

    • Symptom: You can’t reach tenant-a.localhost:4200.
    • Check:
      • Did you save your hosts file correctly? (Requires administrator privileges).
      • Did you restart your browser or clear its DNS cache after editing the hosts file? Sometimes a full system reboot might be necessary if changes don’t propagate.
      • Is your ng serve running on the default port 4200? If not, adjust the URL.
  4. Feature Flags Not Working:

    • Symptom: A feature (e.g., “Advanced Reporting”) is always visible/hidden regardless of tenant config.
    • Check:
      • Is the *ngIf condition correctly bound to the currentTenant?.features.propertyName?
      • Is the features object present and correctly structured in your JSON config files?
      • Is the TenantConfig interface correctly defining the features property?

Summary

Congratulations! You’ve successfully designed and implemented a foundational white-label SaaS UI using modern Angular practices. Here are the key takeaways from this chapter:

  • White-labeling allows a single application to be rebranded for multiple clients, significantly reducing development and maintenance overhead.
  • Runtime multi-tenancy is the preferred architectural pattern for scalable white-label solutions, allowing dynamic customization without requiring separate builds for each tenant.
  • The APP_INITIALIZER token is crucial for ensuring tenant-specific configurations are loaded and applied before the application renders, preventing UI flashes.
  • CSS Custom Properties (variables) are the backbone of dynamic theming, providing a flexible and performant way to apply brand-specific styles at runtime.
  • Tenant identification (e.g., via hostname) is the first step in dynamically loading the correct configuration and assets.

This project demonstrates how architectural decisions around configuration, theming, and application lifecycle can profoundly impact an application’s flexibility, scalability, and maintainability. In a real-world scenario, you would extend this with more robust backend configuration services, potentially a custom build process for tenant-specific assets, and more sophisticated feature flagging.

What’s Next?

In the next chapter, we’ll delve into another critical enterprise scenario: Project: Building an Offline-Capable Field App. This will introduce concepts like Service Workers, local storage, and strategies for graceful degradation, essential for applications operating in environments with unreliable connectivity.

References

  1. Angular Official Documentation: https://angular.io/
  2. MDN Web Docs - CSS Custom Properties: https://developer.mozilla.org/en-US/docs/Web/CSS/–*
  3. Angular CLI Documentation: https://angular.io/cli
  4. Angular APP_INITIALIZER: https://angular.io/api/core/APP_INITIALIZER
  5. Angular Standalone Components: https://angular.io/guide/standalone-components

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.