Dynamic SCSS Loading for Multiple Domains with Shared Ionic Codebase
Managing multiple branded websites or applications that share the same codebase is a common challenge for developers. When you need to maintain different visual identities across multiple domains while keeping the functionality consistent, dynamic SCSS loading is an elegant solution. This article walks through implementing this approach in an Ionic application.
The Challenge
Imagine you’re managing a SaaS platform that serves different clients through their own domains, or perhaps you have a white-label solution that needs to adapt to different branding requirements. The core functionality remains the same, but each domain needs its own:
- Color scheme
- Typography
- Component styling
- Layout variations
- Brand-specific assets
Rather than maintaining separate codebases or using complex build configurations, we can dynamically load domain-specific styles based on the current URL.
Solution Architecture
Our approach consists of four main components:
- A Theme Manager Service that detects the current domain and loads the appropriate styles
- A Structured SCSS Organization with shared and domain-specific variables and styles
- Angular Build Configuration that generates separate CSS bundles
- Application Integration to properly initialize the theme system
Let’s implement each component step by step.
1. Theme Manager Service
The Theme Manager Service detects the current domain and applies the appropriate theme:
typescriptCopy// File: src/theme/theme-manager.service.ts
import { Injectable } from '@angular/core';
import { Platform } from '@ionic/angular';
@Injectable({
providedIn: 'root'
})
export class ThemeManagerService {
private domainThemes = {
'domain1.com': 'domain1',
'domain2.com': 'domain2'
// Add more domains as needed
};
private currentTheme: string;
constructor(private platform: Platform) {
this.initTheme();
}
private initTheme() {
// Get current hostname
const hostname = window.location.hostname;
// Determine theme based on hostname
this.currentTheme = this.domainThemes[hostname] || 'default';
// Apply theme
this.applyTheme(this.currentTheme);
}
private applyTheme(themeName: string) {
// Remove any existing theme class from body
document.body.classList.forEach(className => {
if (className.startsWith('theme-')) {
document.body.classList.remove(className);
}
});
// Add new theme class
document.body.classList.add(`theme-${themeName}`);
// Load the theme stylesheet dynamically
this.loadStylesheet(themeName);
}
private loadStylesheet(themeName: string) {
// Create link element
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = `assets/themes/${themeName}.css`;
linkElement.id = `theme-${themeName}-stylesheet`;
// Remove any previous theme stylesheet
const existingLink = document.getElementById('theme-stylesheet');
if (existingLink) {
existingLink.remove();
}
// Add new stylesheet to head
linkElement.id = 'theme-stylesheet';
document.head.appendChild(linkElement);
}
// Public method to switch theme manually if needed
public switchTheme(themeName: string) {
if (this.domainThemes[themeName] || themeName === 'default') {
this.applyTheme(themeName);
}
}
// Get current theme name
public getCurrentTheme(): string {
return this.currentTheme;
}
}
This service:
- Maps domain names to theme names
- Detects the current domain on initialization
- Applies a domain-specific CSS class to the document body
- Dynamically loads the appropriate stylesheet
- Provides methods for manually switching themes (useful for testing)
2. SCSS Structure
Organizing your SCSS files properly is crucial for maintainability. Here’s a structure that separates shared variables from domain-specific styling:
scssCopy// File: src/theme/variables.scss (Shared variables)
:root {
// Common variables used across all themes
// These would be overridden by domain-specific themes
--ion-font-family: 'Roboto', sans-serif;
--ion-padding: 16px;
--ion-margin: 16px;
// Default theme variables
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
// Other ionic default colors...
}
// File: src/theme/domain1/variables.scss
.theme-domain1 {
--ion-color-primary: #ff4961;
--ion-color-primary-rgb: 255, 73, 97;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #e04055;
--ion-color-primary-tint: #ff5b71;
// Define other colors for domain1
// Domain-specific font or other variables
--ion-font-family: 'Open Sans', sans-serif;
// Custom domain-specific variables
--domain-header-background: #f7f7f7;
--domain-logo-size: 100px;
}
// File: src/theme/domain2/variables.scss
.theme-domain2 {
--ion-color-primary: #2dd36f;
--ion-color-primary-rgb: 45, 211, 111;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #28ba62;
--ion-color-primary-tint: #42d77d;
// Define other colors for domain2
// Domain-specific font or other variables
--ion-font-family: 'Montserrat', sans-serif;
// Custom domain-specific variables
--domain-header-background: #eafbef;
--domain-logo-size: 120px;
}
// File: src/theme/domain1/styles.scss
@import '../variables';
@import './variables';
// Domain1 specific styles
.theme-domain1 {
// Header customization
ion-header {
background-color: var(--domain-header-background);
}
// Logo customization
.logo {
width: var(--domain-logo-size);
height: auto;
}
// Other domain-specific styles
.welcome-card {
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
// Override any component styles specific to this domain
}
// File: src/theme/domain2/styles.scss
@import '../variables';
@import './variables';
// Domain2 specific styles
.theme-domain2 {
// Header customization
ion-header {
background-color: var(--domain-header-background);
border-bottom: 2px solid var(--ion-color-primary);
}
// Logo customization
.logo {
width: var(--domain-logo-size);
height: auto;
}
// Other domain-specific styles
.welcome-card {
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
// Override any component styles specific to this domain
}
This structure:
- Defines global CSS variables in a shared file
- Creates domain-specific variable overrides
- Encapsulates domain-specific styles within their own scoped classes
- Uses the
.theme-domain1
and.theme-domain2
classes to scope styling (which match the classes our service applies to the body)
3. Angular Configuration
To compile these SCSS files into separate bundles that can be loaded dynamically, we need to update the Angular configuration:
jsonCopy// File: angular.json (modified to build theme files)
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"projects": {
"app": {
"architect": {
"build": {
"options": {
"outputPath": "www",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
}
],
"styles": [
{
"input": "src/theme/variables.scss",
"bundleName": "global"
},
{
"input": "src/theme/domain1/styles.scss",
"bundleName": "domain1"
},
{
"input": "src/theme/domain2/styles.scss",
"bundleName": "domain2"
},
{
"input": "src/global.scss",
"bundleName": "global"
}
]
// other options...
}
// other configurations...
}
}
}
}
}
This configuration:
- Creates separate CSS bundles for each domain
- Names the bundles so they can be easily referenced
- Ensures the global styles are always included
4. Application Integration
Finally, we need to integrate the Theme Manager Service into our application:
typescriptCopy// File: src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ThemeManagerService } from '../theme/theme-manager.service';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
ThemeManagerService
],
bootstrap: [AppComponent]
})
export class AppModule {}
// File: src/app/app.component.ts
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { ThemeManagerService } from '../theme/theme-manager.service';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(
private platform: Platform,
private themeManager: ThemeManagerService
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
// Theme manager service will initialize and load appropriate theme
console.log(`Current theme: ${this.themeManager.getCurrentTheme()}`);
});
}
}
And update the index.html file to prepare for dynamic stylesheet loading:
htmlCopy<!-- File: src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- The global stylesheet will be loaded by default -->
<!-- Theme-specific stylesheets will be loaded dynamically by the ThemeManagerService -->
</head>
<body>
<app-root></app-root>
</body>
</html>
Testing and Development Workflow
Local Testing
To test different themes during development:
- Edit your hosts file to create domain aliases that point to localhost: Copy
127.0.0.1 domain1.local 127.0.0.1 domain2.local
- Use the
switchTheme
method for quick testing: typescriptCopy// In a development component: constructor(private themeManager: ThemeManagerService) {} switchToDomain1() { this.themeManager.switchTheme('domain1'); } switchToDomain2() { this.themeManager.switchTheme('domain2'); }
- Create a theme switcher component for easier testing: typescriptCopy
@Component({ selector: 'app-theme-switcher', template: ` <ion-segment (ionChange)="segmentChanged($event)"> <ion-segment-button value="domain1"> <ion-label>Domain 1</ion-label> </ion-segment-button> <ion-segment-button value="domain2"> <ion-label>Domain 2</ion-label> </ion-segment-button> </ion-segment> ` }) export class ThemeSwitcherComponent { constructor(private themeManager: ThemeManagerService) {} segmentChanged(ev: any) { this.themeManager.switchTheme(ev.detail.value); } }
Production Deployment
For production, you’ll need to ensure that your build and deployment pipeline:
- Correctly generates all theme bundles
- Includes these bundles in the deployment package
- Places them in the correct location in your assets folder
- Configures your web server to properly serve these assets
Advanced Techniques
Dynamic Font Loading
If your domains use different fonts, consider implementing dynamic font loading:
typescriptCopyprivate loadDomainFonts(themeName: string) {
const fontFamilies = {
'domain1': ['Open Sans:400,700', 'Roboto:400,500'],
'domain2': ['Montserrat:400,700', 'Lato:400,700']
};
const fonts = fontFamilies[themeName];
if (fonts) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css?family=${fonts.join('|')}&display=swap`;
document.head.appendChild(link);
}
}
Environment-specific Configuration
Create environment-specific domain mappings:
typescriptCopy// environments/environment.ts
export const environment = {
production: false,
domainThemeMap: {
'domain1.local': 'domain1',
'domain2.local': 'domain2',
'localhost': 'domain1' // default for local development
}
};
// environments/environment.prod.ts
export const environment = {
production: true,
domainThemeMap: {
'domain1.com': 'domain1',
'domain2.com': 'domain2'
}
};
Then update the Theme Manager Service:
typescriptCopyimport { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ThemeManagerService {
private domainThemes = environment.domainThemeMap;
// rest of the service...
}
Optimizing Bundle Sizes
To reduce the size of each theme bundle, consider:
- Using CSS Variables extensively: Define colors and other properties as variables in your base theme, then only override what changes in domain-specific themes.
- Isolating domain-specific styles: Only put truly domain-specific styles in the domain bundles, keeping common styling in the global bundle.
- Lazy loading components: If certain components are only used in specific domains, lazy load them with Angular’s routing system.
Best Practices for Multi-domain Styling
- Create a design system first: Define a consistent design system with components, spacing, and typography rules that work across all domains.
- Use CSS variables for everything: Colors, spacing, typography, and any domain-specific value should be a CSS variable.
- Build components to respect theming: Make sure your components read from CSS variables rather than having hardcoded values.
- Keep theme-specific overrides minimal: Try to design your system so domain-specific overrides are exceptions, not the rule.
- Test across all domains regularly: Ensure that changes to shared components work properly across all themed variations.
Conclusion
Dynamic SCSS loading for multiple domains in an Ionic application provides a powerful way to maintain different brand identities while sharing a single codebase. This approach offers several benefits:
- Maintainability: Core functionality changes propagate across all domains automatically
- Performance: Each domain only loads the styles it needs
- Scalability: Adding new domains is as simple as creating new theme files and updating a mapping
- Development efficiency: Developers can work on a single codebase instead of managing multiple versions
By following the patterns outlined in this article, you can create a flexible, maintainable system for managing multiple visual identities across different domains in your Ionic application.