24

Module 24: Styling & Design System Integration (Senior/Enterprise)

Chapter 24 • Advanced

40 min

Styling & Design System Integration (Senior/Enterprise)

The Real Problem

Scenario: Products app uses blue buttons. Checkout app uses green buttons. Admin app uses red buttons. Users see inconsistent UI.

The Question: How do I maintain consistent styling across multiple micro frontends while allowing teams to work independently?

This is design system architecture.


The Production Reality

Styling Challenges

  1. Consistency - All apps should look the same
  2. Independence - Teams need to work independently
  3. Theme Management - Support light/dark themes
  4. CSS Isolation - Styles shouldn't leak between apps
  5. Design System - Shared components and tokens

The Question: How do we solve each challenge?


Strategy 1: Shared Design System Package

The Pattern

Create a shared design system package. All apps consume it.

Step 1: Create Design System Package

bash.js
# Create design system package
mkdir design-system
cd design-system
npm init -y

# Structure
design-system/
├── src/
│   ├── tokens/
│   │   ├── colors.ts
│   │   ├── typography.ts
│   │   └── spacing.ts
│   ├── components/
│   │   ├── button/
│   │   ├── input/
│   │   └── card/
│   └── styles/
│       └── global.css
└── package.json

Step 2: Define Design Tokens

typescript.js
// design-system/src/tokens/colors.ts
export const colors = {
  primary: {
    main: '#1976d2',
    light: '#42a5f5',
    dark: '#1565c0'
  },
  secondary: {
    main: '#dc004e',
    light: '#e33371',
    dark: '#9a0036'
  },
  background: {
    default: '#ffffff',
    paper: '#f5f5f5'
  },
  text: {
    primary: '#212121',
    secondary: '#757575'
  }
} as const
typescript.js
// design-system/src/tokens/typography.ts
export const typography = {
  fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  h1: {
    fontSize: '2.5rem',
    fontWeight: 500,
    lineHeight: 1.2
  },
  h2: {
    fontSize: '2rem',
    fontWeight: 500,
    lineHeight: 1.3
  },
  body1: {
    fontSize: '1rem',
    fontWeight: 400,
    lineHeight: 1.5
  }
} as const

Step 3: Create Shared Components

typescript.js
// design-system/src/components/button/button.component.ts
@Component({
  selector: 'ds-button',
  template: `
    <button 
      [class]="'ds-button ds-button--' + variant"
      [disabled]="disabled"
      (click)="onClick.emit($event)">
      <ng-content></ng-content>
    </button>
  `,
  styles: [`
    .ds-button {
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
      font-family: var(--ds-font-family);
      font-size: var(--ds-font-size-body1);
      cursor: pointer;
      transition: background-color 0.2s;
    }
    
    .ds-button--primary {
      background-color: var(--ds-color-primary-main);
      color: white;
    }
    
    .ds-button--primary:hover:not(:disabled) {
      background-color: var(--ds-color-primary-dark);
    }
    
    .ds-button:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
  `]
})
export class ButtonComponent {
  @Input() variant: 'primary' | 'secondary' = 'primary'
  @Input() disabled = false
  @Output() onClick = new EventEmitter<MouseEvent>()
}

Step 4: Consume in Apps

typescript.js
// products-app/package.json
{
  "dependencies": {
    "@company/design-system": "^1.0.0"
  }
}

// products-app/src/app/app.module.ts
import { DesignSystemModule } from '@company/design-system'

@NgModule({
  imports: [DesignSystemModule],
  // ...
})
export class AppModule {}
typescript.js
// products-app/src/app/products/products.component.html
<ds-button variant="primary" (onClick)="addToCart()">
  Add to Cart
</ds-button>

Strategy 2: CSS Variables for Theming

The Pattern

Use CSS variables for theme values. Change variables to change theme.

Step 1: Define CSS Variables

css.js
/* design-system/src/styles/theme.css */
:root {
  /* Colors */
  --ds-color-primary-main: #1976d2;
  --ds-color-primary-light: #42a5f5;
  --ds-color-primary-dark: #1565c0;
  
  --ds-color-secondary-main: #dc004e;
  --ds-color-secondary-light: #e33371;
  --ds-color-secondary-dark: #9a0036;
  
  --ds-color-background-default: #ffffff;
  --ds-color-background-paper: #f5f5f5;
  
  --ds-color-text-primary: #212121;
  --ds-color-text-secondary: #757575;
  
  /* Typography */
  --ds-font-family: "Roboto", "Helvetica", "Arial", sans-serif;
  --ds-font-size-h1: 2.5rem;
  --ds-font-size-h2: 2rem;
  --ds-font-size-body1: 1rem;
  
  /* Spacing */
  --ds-spacing-xs: 4px;
  --ds-spacing-sm: 8px;
  --ds-spacing-md: 16px;
  --ds-spacing-lg: 24px;
  --ds-spacing-xl: 32px;
}

Step 2: Dark Theme

css.js
/* design-system/src/styles/theme-dark.css */
[data-theme="dark"] {
  --ds-color-primary-main: #42a5f5;
  --ds-color-primary-light: #64b5f6;
  --ds-color-primary-dark: #1976d2;
  
  --ds-color-background-default: #121212;
  --ds-color-background-paper: #1e1e1e;
  
  --ds-color-text-primary: #ffffff;
  --ds-color-text-secondary: #b0b0b0;
}

Step 3: Use in Components

css.js
/* products-app/src/app/products/products.component.css */
.product-card {
  background-color: var(--ds-color-background-paper);
  padding: var(--ds-spacing-md);
  border-radius: 8px;
}

.product-title {
  color: var(--ds-color-text-primary);
  font-size: var(--ds-font-size-h2);
  font-family: var(--ds-font-family);
}

.product-price {
  color: var(--ds-color-primary-main);
  font-size: var(--ds-font-size-body1);
}

Step 4: Toggle Theme

typescript.js
// Host App - Theme service
@Injectable({ providedIn: 'root' })
export class ThemeService {
  private theme$ = new BehaviorSubject<'light' | 'dark'>('light')
  
  constructor() {
    // Load saved theme
    const saved = localStorage.getItem('theme') as 'light' | 'dark'
    if (saved) {
      this.setTheme(saved)
    }
  }
  
  setTheme(theme: 'light' | 'dark') {
    document.documentElement.setAttribute('data-theme', theme)
    this.theme$.next(theme)
    localStorage.setItem('theme', theme)
    
    // Notify remotes
    this.eventBus.emit({
      event: 'theme:changed',
      version: '1.0.0',
      data: { theme }
    })
  }
  
  getTheme(): Observable<'light' | 'dark'> {
    return this.theme$.asObservable()
  }
}

Strategy 3: CSS Isolation

The Problem

Products app styles leak into Checkout app.

Solution 1: ViewEncapsulation (Angular Default)

typescript.js
// Angular automatically scopes styles
@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css'],
  encapsulation: ViewEncapsulation.Emulated // Default
})
export class ProductsComponent {}

Why This Works:

  • ✅ Styles scoped to component
  • ✅ No style leakage
  • ✅ Works automatically

Solution 2: Shadow DOM (True Isolation)

typescript.js
@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css'],
  encapsulation: ViewEncapsulation.ShadowDom
})
export class ProductsComponent {}

Why This Works:

  • ✅ True isolation
  • ✅ Styles can't leak
  • ⚠️ CSS variables don't work (need special handling)

Solution 3: CSS Modules

typescript.js
// products.component.css
.productCard {
  background: white;
  padding: 16px;
}

// products.component.ts
@Component({
  template: `
    <div [class]="styles.productCard">
      Product
    </div>
  `
})
export class ProductsComponent {
  styles = styles // Import from CSS module
}

Strategy 4: Shared Styles Package

The Pattern

Package shared styles. Apps import what they need.

Step 1: Create Styles Package

typescript.js
// shared-styles/src/reset.css
/* CSS Reset */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

// shared-styles/src/utilities.css
.ds-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--ds-spacing-md);
}

.ds-flex {
  display: flex;
}

.ds-flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

Step 2: Import in Apps

typescript.js
// products-app/src/styles.css
@import '@company/shared-styles/reset.css';
@import '@company/shared-styles/utilities.css';
@import '@company/design-system/theme.css';

Strategy 5: Design System as Remote

The Pattern

Expose design system as Module Federation remote.

Step 1: Configure Design System as Remote

typescript.js
// design-system/webpack.config.js
new ModuleFederationPlugin({
  name: 'design-system',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/button/button.component.ts',
    './Input': './src/components/input/input.component.ts',
    './Card': './src/components/card/card.component.ts',
    './Theme': './src/styles/theme.css'
  },
  shared: {
    '@angular/core': { singleton: true, strictVersion: true }
  }
})

Step 2: Consume in Apps

typescript.js
// products-app/webpack.config.js
new ModuleFederationPlugin({
  name: 'products',
  remotes: {
    'design-system': 'design-system@https://design-system.example.com/remoteEntry.js'
  }
})
typescript.js
// products-app/src/app/products/products.component.ts
import { ButtonComponent } from 'design-system/Button'

@Component({
  imports: [ButtonComponent],
  template: `
    <ds-button>Add to Cart</ds-button>
  `
})
export class ProductsComponent {}

Production Checklist

  • [ ] Design system package created
  • [ ] Design tokens defined
  • [ ] Shared components built
  • [ ] CSS variables for theming
  • [ ] Theme switching implemented
  • [ ] CSS isolation configured
  • [ ] Shared styles packaged
  • [ ] Documentation for teams

Key Takeaways

  1. Shared design system - Single source of truth
  2. CSS variables - Easy theming
  3. CSS isolation - Prevent style leakage
  4. Package styles - Reusable across apps
  5. Documentation - Teams need guidelines

Remember: Consistent styling requires architecture, not just CSS.

The next module: Type-safe Module Federation.