Skip to content

Theming token system for the Guardian frontend #6091

@danielnorkin

Description

@danielnorkin

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:

  1. 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.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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?
  4. 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.
  5. PrimeNG trajectory. If the refresh is moving off PrimeNG, guardian.prime.scss is dead weight. Worth confirming before we polish the PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions