From SASS to CSS Custom Properties: Theming a Large Angular App and Its Libraries

Building a theme system for a large Angular application (100+ modules) with both light and dark modes led our team to replace SASS variables with CSS3 custom properties — a move that simplified maintenance and improved performance.

Our previous theming solution relied on SASS variables combined with Angular's :host-context selector. After migrating to a CSS3 variable-based approach, we saw significant gains in developer experience, bundle size, and reliability. Although the examples here are Angular-specific, the underlying technique applies to any web application.

The SASS Approach and Its Shortcomings

Originally, a class on the root element — <body> in our case — identified the active theme.

:host-context(.light-theme)::ng-deep {
  my-component-selector {
    color: light-theme-color;
  }
}

:host-context(.dark-theme)::ng-deep {
  my-component-selector {
    color: dark-theme-color;
  }
}

Handling light and dark themes with SASS

While this approach successfully changed visual styles on theme switch, several problems became apparent as the application scaled:

  1. Developer burden: Ensuring every component worked under both themes required heavy reliance on code reviews and manual testing. Despite the effort, bugs still slipped through.
  2. Performance penalty: Styles for each theme were compiled and bundled together, inflating the bundle size even though only one theme's styles were ever active at a time.
  3. Color governance: Keeping component colors within a defined palette was difficult to enforce.

CSS3 Custom Properties: A Better Way

Research into alternative solutions led us to CSS3 custom properties — often called CSS variables.

Unlike SASS variables, which resolve at compile time, CSS3 variables are a native browser feature. This difference brings several advantages:

  1. Simpler maintenance: Custom property values can be overridden without creating separate variable sets per theme.
  2. Quick onboarding: Adding a new theme is straightforward — just supply a file with updated values for the existing variable set.
  3. External configuration: Variable values can be pulled from a TypeScript file, an API response, or another external source. This is valuable for products that let partners or customers define custom themes.
  4. Consistent behavior: Components that rely on CSS3 variables automatically respect the theme as long as they use the defined variables — no per-theme code paths to forget.
  5. Efficiency: Only one set of variables lives in the browser at a time; the active set can be swapped on theme change.

Important: This technique is not compatible with Internet Explorer 11. Check browser support for CSS variables before adopting it.

Implementation

Our application is split into a parent app and several shared Angular libraries. For complete theme coverage, both the app and its libraries had to support light and dark modes. Variables are defined only at the root level, not on individual components.

Each part of the codebase defines its own theme variable files. Below are examples for the parent app; the shared libraries follow the same pattern.

Light Theme: Color definitions for light mode

:root {
    --primary-background-color: #FFFFFF
    --secondary-background-color: #FFFFFF
    --primary-border-color: #CCCCCC
    --error-border-color: #FF0000
    --text-color: #000000
    --sub-text-color: #737373
}

Dark Theme: Color definitions for dark mode

:root {
    --primary-background-color: #000000
    --secondary-background-color: #566572
    --primary-border-color: #17242B
    --error-border-color: #F54F47
    --text-color: #FFFFFF
    --sub-text-color: #ADBBC4
}

Example component style:

:host {
  background-color: var(--primary-background-color);

  p {
    color: var(--text-color);
  }
}

Importing shared library styles into the parent app:

For components living in shared libraries to be theme-aware, each library must output a file per theme containing the CSS3 variables it uses. These files are then imported into the parent app's theme files.

Our light-theme.scss ends up looking like this:

@import "shared-library/library-light-theme.scss";

:root {
    --primary-background-color: #FFFFFF
    --secondary-background-color: #FFFFFF
    --primary-border-color: #CCCCCC
    --error-border-color: #FF0000
    --text-color: #000000
    --sub-text-color: #737373
}

Note: Libraries must expose their style assets in the distribution output.

For Angular 9 and later, configure these assets in ng-package.json:

"assets": ["./src/lib/style"]

For Angular 8 and earlier, use the cpx module to copy style files into the build output:

cpx './projects/shared-library/src/lib/style/*' './dist/shared-library/style'

Each shared library thus contributes a set of theme-specific CSS3 variable files, which are aggregated in the main application.

Where We Stand Today

  1. SASS removal: All theming-related SASS code has been removed; the application now relies exclusively on CSS3 variables.
  2. Enforced color palette: We now have clear control over which colors appear throughout the app, with standardized usage.
  3. Static checks: A style lint rule enforces that only CSS3 variables define colors, catching violations at compile time.
  4. Improved developer experience: Building new components no longer requires extra effort to support multiple themes — the variable system handles it automatically.
  5. Framework independence: The same pattern works across Angular and non-Angular projects, given modern browser support for CSS3 variables.
  6. Production proven: The application is live in production with positive user feedback and no reported theming bugs so far.
  7. Next steps: We plan to add compile-time validation to guarantee consistent variable sets across themes, define fallback colors, and catch any undefined variable references.