Summary
Sharing a design for a token-driven theming layer in the Guardian Angular frontend that lets deployers white-label Guardian under their own brand without forking component SCSS. Filing this ahead of a PR so we can align on naming and scope before code review starts. A reference branch is linked below for direct preview.
Why
Three problems we've all seen across the codebase:
- White-label deployers carry permanent fork divergence. Anyone shipping Guardian under their own brand (Climission and others) maintains a patched fork that touches dozens of component files. Each UI change risks breaking those patches.
- Runtime tenant branding only partially propagates.
BrandingService writes --color-primary onto document.body, but many components ignore that variable and reference hard-coded hex values, so per-tenant brand overrides apply inconsistently.
- The upcoming UI refresh will re-fragment branding if it inherits hard-coded values in component styles. Landing the token abstraction first means the refresh gets built on top of a shared theming layer rather than around it.
A token system solves all three at once and gives Guardian a first-class white-label story for new commercial deployers.
Design
Single source of truth. A new file frontend/src/styles/guardian-tokens.scss declares every brand value (colors, surfaces, text, borders, gray scale, semantic colors, typography scale + weights + tracking, radii, spacing, timings + easings, shadows, gradients, layout dimensions) as --guardian-* CSS custom properties.
Aliasing layer. frontend/src/variables.scss is rewritten so every legacy variable (--color-*, --primary-color, --linear-gradient, --header-*, --button-*) resolves to a token. Default token values match the values currently hard-coded in variables.scss, so the visual diff on the day this lands is zero.
Token-driven style layer. Seven new files under frontend/src/styles/:
| File |
What it controls |
guardian.fonts.scss |
.mat-typography sizing and weight ramp via tokens |
guardian.inputs.scss |
Form inputs, opt-in via .guardian scope |
guardian.banner.scss |
Alert banner variants (important / warning / low / info / star) |
guardian.dialog.scss |
Material and PrimeNG dialog treatments (opt-in via class) |
guardian.feedback.scss |
Unified 3-tier loader system (action arc, skeleton, large ring), PrimeNG progress bar with shimmer, shared .loading wrapper |
guardian.prime.scss |
PrimeNG component overrides through tokens (inputs, dropdowns, multi-selects, checkboxes, buttons, tabs, tables, tree-tables, paginators). Also fixes the "box in a box" double-border PrimeNG renders for nested .p-inputtext inside .p-dropdown / .p-multiselect. |
guardian.patterns.scss |
Reusable admin layout primitives (page hero, stats grid, cards, badges, filter rows, stages with stagger animations, scroll-hint, user picker). All opt-in. |
BrandingService refactor (frontend/src/app/services/branding.service.ts):
- Adds
PLATFORM_CHROME_ROUTE_PREFIXES (route allowlist for "platform-owned" surfaces like /login, /admin, /branding) and PLATFORM_CHROME_ROLES (operator-role allowlist) as module-level exports that deployers can extend or replace.
applyBrandingForRoute(url, role) decides whether to apply tenant branding or platform-default branding based on both signals. Solves the case where an admin user navigates to a tenant route and shouldn't see the tenant's brand leak into operator chrome.
applyPrimaryColor and applyHeaderGradient extracted as public helpers, so the Branding Settings "preview" flow and the production apply flow share one implementation and cannot drift.
- Existing API (
loadBrandingData, saveBrandingData, applyBranding) is unchanged. Backwards compatible.
DefaultBrandings in @guardian/interfaces. A small constant exposing the platform-default brand payload (headerColor, headerColor1, primaryColor, companyName, companyLogoUrl, loginBannerUrl, faviconUrl) so frontend and backend agree on the fallback applied when no tenant branding is loaded.
Documentation. A new frontend/THEMING-GUIDE.md covering the token chain, how to white-label (compile-time defaults + runtime fallback), the platform-chrome vs tenant-workspace distinction, preview / production parity, and author guidelines for new components.
Out of scope for this change
- Brand-specific visual treatments. No deployer logos, custom fonts, brand-specific imagery, or cinematic surfaces. The defaults are Guardian's existing palette and fonts.
- App-shell wiring for
applyBrandingForRoute. The method is in place but no NavigationEnd / auth-change subscriptions are added. Easy follow-up if useful.
- New PrimeNG features. This is theming only. No new components.
Backwards compatibility
The token layer is additive:
- Every component that hard-codes
--color-primary, --primary-color, --linear-gradient, --header-*, --button-* continues to work unchanged. Those names are aliased to tokens in variables.scss.
- The runtime branding override path (BrandingService writing onto
document.body) targets the same legacy variable names. Tenants who have set custom brand colors see no behavior change.
- All new style files are opt-in. No existing page is restyled by this change. Components migrate to the new classes incrementally if and when they're touched.
Reference branch
Four commits, broken roughly along the natural scope splits below.
Open questions
- Timing relative to the UI refresh. Ideally tokens land before the refresh branch cuts, so the new UI is built on the abstraction rather than around it. If the refresh branch is imminent, flag here so we can fast-track.
- Naming. Chose
--guardian-* for the token prefix and guardian.*.scss for the style files. If a different convention fits the project better (--gx-*, --theme-*, frontend/src/theme/), call it now and we'll rename across the branch.
- Scope split. Can land as one PR or split into (a) tokens + variables.scss aliasing, (b) style layer, (c) BrandingService refactor + DefaultBrandings, (d) docs. (a) alone delivers most of the value. Reviewer preference?
- Patterns library.
guardian.patterns.scss is the most opinionated piece. Happy to drop it from the first PR if we'd rather settle the tokens + Prime overrides first and bring patterns in as a follow-up.
- PrimeNG trajectory. If the refresh is moving off PrimeNG,
guardian.prime.scss is dead weight. Worth confirming before we polish the PR.
Summary
Sharing a design for a token-driven theming layer in the Guardian Angular frontend that lets deployers white-label Guardian under their own brand without forking component SCSS. Filing this ahead of a PR so we can align on naming and scope before code review starts. A reference branch is linked below for direct preview.
Why
Three problems we've all seen across the codebase:
BrandingServicewrites--color-primaryontodocument.body, but many components ignore that variable and reference hard-coded hex values, so per-tenant brand overrides apply inconsistently.A token system solves all three at once and gives Guardian a first-class white-label story for new commercial deployers.
Design
Single source of truth. A new file
frontend/src/styles/guardian-tokens.scssdeclares every brand value (colors, surfaces, text, borders, gray scale, semantic colors, typography scale + weights + tracking, radii, spacing, timings + easings, shadows, gradients, layout dimensions) as--guardian-*CSS custom properties.Aliasing layer.
frontend/src/variables.scssis rewritten so every legacy variable (--color-*,--primary-color,--linear-gradient,--header-*,--button-*) resolves to a token. Default token values match the values currently hard-coded invariables.scss, so the visual diff on the day this lands is zero.Token-driven style layer. Seven new files under
frontend/src/styles/:guardian.fonts.scss.mat-typographysizing and weight ramp via tokensguardian.inputs.scss.guardianscopeguardian.banner.scssguardian.dialog.scssguardian.feedback.scss.loadingwrapperguardian.prime.scss.p-inputtextinside.p-dropdown/.p-multiselect.guardian.patterns.scssBrandingService refactor (
frontend/src/app/services/branding.service.ts):PLATFORM_CHROME_ROUTE_PREFIXES(route allowlist for "platform-owned" surfaces like/login,/admin,/branding) andPLATFORM_CHROME_ROLES(operator-role allowlist) as module-level exports that deployers can extend or replace.applyBrandingForRoute(url, role)decides whether to apply tenant branding or platform-default branding based on both signals. Solves the case where an admin user navigates to a tenant route and shouldn't see the tenant's brand leak into operator chrome.applyPrimaryColorandapplyHeaderGradientextracted as public helpers, so the Branding Settings "preview" flow and the production apply flow share one implementation and cannot drift.loadBrandingData,saveBrandingData,applyBranding) is unchanged. Backwards compatible.DefaultBrandingsin@guardian/interfaces. A small constant exposing the platform-default brand payload (headerColor,headerColor1,primaryColor,companyName,companyLogoUrl,loginBannerUrl,faviconUrl) so frontend and backend agree on the fallback applied when no tenant branding is loaded.Documentation. A new
frontend/THEMING-GUIDE.mdcovering the token chain, how to white-label (compile-time defaults + runtime fallback), the platform-chrome vs tenant-workspace distinction, preview / production parity, and author guidelines for new components.Out of scope for this change
applyBrandingForRoute. The method is in place but noNavigationEnd/ auth-change subscriptions are added. Easy follow-up if useful.Backwards compatibility
The token layer is additive:
--color-primary,--primary-color,--linear-gradient,--header-*,--button-*continues to work unchanged. Those names are aliased to tokens invariables.scss.document.body) targets the same legacy variable names. Tenants who have set custom brand colors see no behavior change.Reference branch
develop: Climission/guardian@develop...feat/theming-tokensFour commits, broken roughly along the natural scope splits below.
Open questions
--guardian-*for the token prefix andguardian.*.scssfor the style files. If a different convention fits the project better (--gx-*,--theme-*,frontend/src/theme/), call it now and we'll rename across the branch.guardian.patterns.scssis the most opinionated piece. Happy to drop it from the first PR if we'd rather settle the tokens + Prime overrides first and bring patterns in as a follow-up.guardian.prime.scssis dead weight. Worth confirming before we polish the PR.